From 2cd65939d54e725cca62d1ad170a2d69318f112a Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 28 Oct 2022 14:54:55 -0400 Subject: [PATCH] Two-Step Login (#3852) * [SG-163] Two step login flow web (#3648) * two step login flow * moved code from old branch and reafctored * fixed review comments * [SG-164] Two Step Login Flow - Browser (#3793) * Add new messages * Remove SSO button from home component * Change create account button to text * Add top padding to create account link * Add email input to HomeComponent * Add continue button to email input * Add form to home component * Retreive email from state service * Redirect to login after submit * Add error message for invalid email * Remove email input from login component * Remove loggingInTo from under MP input * Style the MP hint link * Add self hosted domain to email form * Made the mp hint link bold * Add the new login button * Style app-private-mode-warning in its component * Bitwarden -> Login text change * Remove the old login button * Cancel -> Close text change * Add avatar to login header * Login -> LoginWithMasterPassword text change * Add SSO button to login screen * Add not you button * Allow all clients to use the email query param on the login component * Introduct HomeGuard * Clear remembered email when clicking Not You * Make remember email opt-in * Use formGroup.patchValue instead of directly patching individual controls * [SG-165] Desktop login flow changes (#3814) * two step login flow * moved code from old branch and reafctored * fixed review comments * Make toggleValidateEmail in base class public * Add desktop login messages * Desktop login flow changes * Fix known device api error * Only submit if email has been validated * Clear remembered email when switching accounts * Fix merge issue * Add 'login with another device' button * Remove 'log in with another device' button for now * Pin login pag content to top instead of center justified * Leave email if 'Not you?' is clicked * Continue when enter is hit on email input Co-authored-by: gbubemismith * [SG-750] and [SG-751] Web two step login bug fixes (#3843) * Continue when enter is hit on email input * Mark email input as touched on 'continue' so field is validated * disable login with device on self-hosted (#3895) * [SG-753] Keep email after hint component is launched in browser (#3883) * Keep email after hint component is launched in browser * Use query params instead of state for consistency * Send email and rememberEmail to home component on navigation (#3897) * removed avatar and close button from the password screen (#3901) * [SG-781] Remove extra login page and remove rememberEmail code (#3902) * Remove browser home guard * Always remember email for browser * Remove login landing page button * [SG-782] Add login service to streamline login form data persistence (#3911) * Add login service and abstraction * Inject login service into apps * Inject and use new service in login component * Use service in hint component to prefill email * Add method in LoginService to clear service values * Add LoginService to two-factor component to clear values * make login.service variables private Co-authored-by: Gbubemi Smith Co-authored-by: Addison Beck Co-authored-by: Robyn MacCallum Co-authored-by: gbubemismith --- apps/browser/src/_locales/en/messages.json | 15 ++ .../src/popup/accounts/hint.component.html | 4 +- .../src/popup/accounts/hint.component.ts | 13 +- .../src/popup/accounts/home.component.html | 31 ++- .../src/popup/accounts/home.component.ts | 81 ++++--- .../src/popup/accounts/login.component.html | 43 ++-- .../src/popup/accounts/login.component.ts | 61 +++++- .../popup/accounts/two-factor.component.ts | 8 +- .../private-mode-warning.component.html | 2 +- apps/browser/src/popup/scss/base.scss | 9 + apps/browser/src/popup/scss/box.scss | 5 + apps/browser/src/popup/scss/misc.scss | 4 + apps/browser/src/popup/scss/pages.scss | 10 +- .../src/popup/services/services.module.ts | 6 + .../src/app/accounts/hint.component.ts | 6 +- .../src/app/accounts/login.component.html | 165 ++++++++++----- .../src/app/accounts/login.component.ts | 45 +++- .../src/app/accounts/two-factor.component.ts | 8 +- .../layout/account-switcher.component.html | 2 +- .../app/layout/account-switcher.component.ts | 4 + .../src/app/services/services.module.ts | 6 + apps/desktop/src/locales/en/messages.json | 27 +++ apps/desktop/src/scss/misc.scss | 9 + apps/desktop/src/scss/pages.scss | 2 + apps/web/src/app/accounts/hint.component.ts | 6 +- .../app/accounts/login/login.component.html | 200 ++++++++++-------- .../src/app/accounts/login/login.component.ts | 22 +- .../src/app/accounts/two-factor.component.ts | 8 +- apps/web/src/app/core/core.module.ts | 6 + apps/web/src/locales/en/messages.json | 13 +- libs/angular/src/components/hint.component.ts | 12 +- .../angular/src/components/login.component.ts | 85 +++++++- .../src/components/two-factor.component.ts | 7 +- .../src/services/jslib-services.module.ts | 6 + libs/common/src/abstractions/api.service.ts | 1 + libs/common/src/abstractions/login.service.ts | 7 + libs/common/src/services/api.service.ts | 6 + libs/common/src/services/login.service.ts | 27 +++ 38 files changed, 703 insertions(+), 269 deletions(-) create mode 100644 libs/common/src/abstractions/login.service.ts create mode 100644 libs/common/src/services/login.service.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 7a320e409e..c166273e35 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2028,5 +2028,20 @@ "example": "Jun 15, 2015" } } + }, + "loginWithMasterPassword": { + "message": "Log in with master password" + }, + "loggingInAs": { + "message": "Logging in as" + }, + "notYou": { + "message": "Not you?" + }, + "newAroundHere": { + "message": "New around here?" + }, + "rememberEmail": { + "message": "Remember email" } } diff --git a/apps/browser/src/popup/accounts/hint.component.html b/apps/browser/src/popup/accounts/hint.component.html index 828af9c558..4f5f975ccf 100644 --- a/apps/browser/src/popup/accounts/hint.component.html +++ b/apps/browser/src/popup/accounts/hint.component.html @@ -1,7 +1,9 @@
- +

{{ "passwordHint" | i18n }} diff --git a/apps/browser/src/popup/accounts/hint.component.ts b/apps/browser/src/popup/accounts/hint.component.ts index 7ecc9ef1cb..4948407e5a 100644 --- a/apps/browser/src/popup/accounts/hint.component.ts +++ b/apps/browser/src/popup/accounts/hint.component.ts @@ -1,10 +1,11 @@ import { Component } from "@angular/core"; -import { Router } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { HintComponent as BaseHintComponent } from "@bitwarden/angular/components/hint.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService } from "@bitwarden/common/abstractions/login.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @Component({ @@ -17,8 +18,14 @@ export class HintComponent extends BaseHintComponent { platformUtilsService: PlatformUtilsService, i18nService: I18nService, apiService: ApiService, - logService: LogService + logService: LogService, + private route: ActivatedRoute, + loginService: LoginService ) { - super(router, i18nService, apiService, platformUtilsService, logService); + super(router, i18nService, apiService, platformUtilsService, logService, loginService); + + super.onSuccessfulSubmit = async () => { + this.router.navigate([this.successRoute]); + }; } } diff --git a/apps/browser/src/popup/accounts/home.component.html b/apps/browser/src/popup/accounts/home.component.html index 5f80204a27..46eeb92e5b 100644 --- a/apps/browser/src/popup/accounts/home.component.html +++ b/apps/browser/src/popup/accounts/home.component.html @@ -2,15 +2,28 @@

{{ "loginOrCreateNewAccount" | i18n }}

- - - + +
+
+
+ + +
+
+ +
+
+ +
+ +
- -

- {{ "appName" | i18n }} +

+ {{ "logIn" | i18n }}

-
- -
-
- - -
@@ -52,13 +39,27 @@
+
-

- {{ "loggingInTo" | i18n: selfHostedDomain }} -

-

- -

+
diff --git a/apps/browser/src/popup/accounts/login.component.ts b/apps/browser/src/popup/accounts/login.component.ts index 5028af06bf..d44847b35e 100644 --- a/apps/browser/src/popup/accounts/login.component.ts +++ b/apps/browser/src/popup/accounts/login.component.ts @@ -1,27 +1,33 @@ import { Component, NgZone } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Router } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AppIdService } from "@bitwarden/common/abstractions/appId.service"; import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService } from "@bitwarden/common/abstractions/login.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; +import { Utils } from "@bitwarden/common/misc/utils"; @Component({ selector: "app-login", templateUrl: "login.component.html", }) export class LoginComponent extends BaseLoginComponent { - protected alwaysRememberEmail = true; + protected skipRememberEmail = true; constructor( + apiService: ApiService, + appIdService: AppIdService, authService: AuthService, router: Router, protected platformUtilsService: PlatformUtilsService, @@ -34,9 +40,13 @@ export class LoginComponent extends BaseLoginComponent { logService: LogService, ngZone: NgZone, formBuilder: FormBuilder, - formValidationErrorService: FormValidationErrorsService + formValidationErrorService: FormValidationErrorsService, + route: ActivatedRoute, + loginService: LoginService ) { super( + apiService, + appIdService, authService, router, platformUtilsService, @@ -48,7 +58,9 @@ export class LoginComponent extends BaseLoginComponent { logService, ngZone, formBuilder, - formValidationErrorService + formValidationErrorService, + route, + loginService ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); @@ -59,4 +71,45 @@ export class LoginComponent extends BaseLoginComponent { settings() { this.router.navigate(["environment"]); } + + async launchSsoBrowser() { + // Generate necessary sso params + const passwordOptions: any = { + type: "password", + length: 64, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + }; + + const state = + (await this.passwordGenerationService.generatePassword(passwordOptions)) + + ":clientId=browser"; + const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256"); + const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + + await this.stateService.setSsoCodeVerifier(codeVerifier); + await this.stateService.setSsoState(state); + + let url = this.environmentService.getWebVaultUrl(); + if (url == null) { + url = "https://vault.bitwarden.com"; + } + + const redirectUri = url + "/sso-connector.html"; + + // Launch browser + this.platformUtilsService.launchUri( + url + + "/#/sso?clientId=browser" + + "&redirectUri=" + + encodeURIComponent(redirectUri) + + "&state=" + + state + + "&codeChallenge=" + + codeChallenge + ); + } } diff --git a/apps/browser/src/popup/accounts/two-factor.component.ts b/apps/browser/src/popup/accounts/two-factor.component.ts index 55294ef2e1..5bfb637e4c 100644 --- a/apps/browser/src/popup/accounts/two-factor.component.ts +++ b/apps/browser/src/popup/accounts/two-factor.component.ts @@ -10,6 +10,7 @@ import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.s import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService } from "@bitwarden/common/abstractions/login.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; @@ -44,7 +45,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { private messagingService: MessagingService, logService: LogService, twoFactorService: TwoFactorService, - appIdService: AppIdService + appIdService: AppIdService, + loginService: LoginService ) { super( authService, @@ -58,9 +60,11 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { route, logService, twoFactorService, - appIdService + appIdService, + loginService ); super.onSuccessfulLogin = () => { + this.loginService.clearValues(); return syncService.fullSync(true); }; super.successRoute = "/tabs/vault"; diff --git a/apps/browser/src/popup/components/private-mode-warning.component.html b/apps/browser/src/popup/components/private-mode-warning.component.html index 848b69c92d..5acbb1acd0 100644 --- a/apps/browser/src/popup/components/private-mode-warning.component.html +++ b/apps/browser/src/popup/components/private-mode-warning.component.html @@ -1,4 +1,4 @@ - + {{ "privateModeWarning" | i18n }} {{ "learnMore" | i18n diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 1222f66aaa..6cfa632451 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -174,6 +174,11 @@ header { .right { justify-content: flex-end; + align-items: center; + app-avatar { + max-height: 30px; + margin-right: 5px; + } } .center { @@ -183,6 +188,10 @@ header { min-width: 0; } + .login-center { + margin: auto; + } + app-pop-out > button, div > button, div > a { diff --git a/apps/browser/src/popup/scss/box.scss b/apps/browser/src/popup/scss/box.scss index 7be206cbf3..844a397a14 100644 --- a/apps/browser/src/popup/scss/box.scss +++ b/apps/browser/src/popup/scss/box.scss @@ -83,6 +83,11 @@ margin: 5px 10px; font-size: $font-size-small; + button.btn { + font-size: $font-size-small; + padding: 0; + } + @include themify($themes) { color: themed("mutedColor"); } diff --git a/apps/browser/src/popup/scss/misc.scss b/apps/browser/src/popup/scss/misc.scss index a4d5dcba96..d891731c75 100644 --- a/apps/browser/src/popup/scss/misc.scss +++ b/apps/browser/src/popup/scss/misc.scss @@ -440,3 +440,7 @@ app-vault-view .box-footer { html.force_redraw { animation: redraw 1s linear infinite; } + +.rounded-circle { + border-radius: 50% !important; +} diff --git a/apps/browser/src/popup/scss/pages.scss b/apps/browser/src/popup/scss/pages.scss index 9ff96f048b..ddba48f27a 100644 --- a/apps/browser/src/popup/scss/pages.scss +++ b/apps/browser/src/popup/scss/pages.scss @@ -88,7 +88,7 @@ app-home { } } -app-private-mode-warning { +.app-private-mode-warning { display: block; padding-top: 1rem; } @@ -115,3 +115,11 @@ body.body-full { } } } + +.createAccountLink { + padding-top: 30px; +} + +.login-buttons > button { + margin: 15px 0 15px 0; +} diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index da96f7f903..236ff4e5b1 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -24,6 +24,7 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/abstractions/log.service"; +import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/abstractions/login.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; @@ -48,6 +49,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service"; import { AuthService } from "@bitwarden/common/services/auth.service"; import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service"; +import { LoginService } from "@bitwarden/common/services/login.service"; import { SearchService } from "@bitwarden/common/services/search.service"; import MainBackground from "../../background/main.background"; @@ -309,6 +311,10 @@ function getBgService(service: keyof MainBackground) { provide: FileDownloadService, useClass: BrowserFileDownloadService, }, + { + provide: LoginServiceAbstraction, + useClass: LoginService, + }, { provide: AbstractThemingService, useFactory: () => { diff --git a/apps/desktop/src/app/accounts/hint.component.ts b/apps/desktop/src/app/accounts/hint.component.ts index 7ecc9ef1cb..8987efd617 100644 --- a/apps/desktop/src/app/accounts/hint.component.ts +++ b/apps/desktop/src/app/accounts/hint.component.ts @@ -5,6 +5,7 @@ import { HintComponent as BaseHintComponent } from "@bitwarden/angular/component import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService } from "@bitwarden/common/abstractions/login.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @Component({ @@ -17,8 +18,9 @@ export class HintComponent extends BaseHintComponent { platformUtilsService: PlatformUtilsService, i18nService: I18nService, apiService: ApiService, - logService: LogService + logService: LogService, + loginService: LoginService ) { - super(router, i18nService, apiService, platformUtilsService, logService); + super(router, i18nService, apiService, platformUtilsService, logService, loginService); } } diff --git a/apps/desktop/src/app/accounts/login.component.html b/apps/desktop/src/app/accounts/login.component.html index c11ed881b0..b81ef6ee5a 100644 --- a/apps/desktop/src/app/accounts/login.component.html +++ b/apps/desktop/src/app/accounts/login.component.html @@ -22,78 +22,135 @@
Bitwarden

{{ "loginOrCreateNewAccount" | i18n }}

-
-
-
- - -
-
- diff --git a/apps/desktop/src/app/accounts/login.component.ts b/apps/desktop/src/app/accounts/login.component.ts index 33eefbd57e..43909423ee 100644 --- a/apps/desktop/src/app/accounts/login.component.ts +++ b/apps/desktop/src/app/accounts/login.component.ts @@ -1,9 +1,11 @@ import { Component, NgZone, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Router } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component"; import { ModalService } from "@bitwarden/angular/services/modal.service"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AppIdService } from "@bitwarden/common/abstractions/appId.service"; import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service"; @@ -11,6 +13,7 @@ import { EnvironmentService } from "@bitwarden/common/abstractions/environment.s import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService } from "@bitwarden/common/abstractions/login.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -29,13 +32,23 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { @ViewChild("environment", { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef; - showingModal = false; + webVaultHostname = ""; - protected alwaysRememberEmail = true; + showingModal = false; private deferFocus: boolean = null; + get loggedEmail() { + return this.formGroup.value.email; + } + + get selfHostedDomain() { + return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null; + } + constructor( + apiService: ApiService, + appIdService: AppIdService, authService: AuthService, router: Router, i18nService: I18nService, @@ -51,9 +64,13 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { private messagingService: MessagingService, logService: LogService, formBuilder: FormBuilder, - formValidationErrorService: FormValidationErrorsService + formValidationErrorService: FormValidationErrorsService, + route: ActivatedRoute, + loginService: LoginService ) { super( + apiService, + appIdService, authService, router, platformUtilsService, @@ -65,7 +82,9 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { logService, ngZone, formBuilder, - formValidationErrorService + formValidationErrorService, + route, + loginService ); super.onSuccessfulLogin = () => { return syncService.fullSync(true); @@ -127,7 +146,23 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { this.showPassword = false; } + async continue() { + await super.validateEmail(); + if (!this.formGroup.controls.email.valid) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccured"), + this.i18nService.t("invalidEmail") + ); + return; + } + } + async submit() { + if (!this.validatedEmail) { + return; + } + await super.submit(); if (this.captchaSiteKey) { const content = document.getElementById("content") as HTMLDivElement; diff --git a/apps/desktop/src/app/accounts/two-factor.component.ts b/apps/desktop/src/app/accounts/two-factor.component.ts index 51ad6e1329..858ddfc5ae 100644 --- a/apps/desktop/src/app/accounts/two-factor.component.ts +++ b/apps/desktop/src/app/accounts/two-factor.component.ts @@ -9,6 +9,7 @@ import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService } from "@bitwarden/common/abstractions/login.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; @@ -41,7 +42,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { route: ActivatedRoute, logService: LogService, twoFactorService: TwoFactorService, - appIdService: AppIdService + appIdService: AppIdService, + loginService: LoginService ) { super( authService, @@ -55,9 +57,11 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { route, logService, twoFactorService, - appIdService + appIdService, + loginService ); super.onSuccessfulLogin = () => { + this.loginService.clearValues(); return syncService.fullSync(true); }; } diff --git a/apps/desktop/src/app/layout/account-switcher.component.html b/apps/desktop/src/app/layout/account-switcher.component.html index 9b77638a6c..4080b2e031 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.html +++ b/apps/desktop/src/app/layout/account-switcher.component.html @@ -90,7 +90,7 @@
- diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index 0f03464acb..0c1ca74bd5 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -1,6 +1,7 @@ import { animate, state, style, transition, trigger } from "@angular/animations"; import { ConnectedPosition } from "@angular/cdk/overlay"; import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; import { concatMap, Subject, takeUntil } from "rxjs"; import { AuthService } from "@bitwarden/common/abstractions/auth.service"; @@ -91,6 +92,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { private stateService: StateService, private authService: AuthService, private messagingService: MessagingService, + private router: Router, private tokenService: TokenService ) {} @@ -142,6 +144,8 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { async addAccount() { this.close(); await this.stateService.setActiveUser(null); + await this.stateService.setRememberedEmail(null); + this.router.navigate(["/login"]); } private async createSwitcherAccounts(baseAccounts: { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index aa4e921089..6dc9b8b0db 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -23,6 +23,7 @@ import { LogService, LogService as LogServiceAbstraction, } from "@bitwarden/common/abstractions/log.service"; +import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/abstractions/login.service"; import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service"; import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service"; @@ -35,6 +36,7 @@ import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/abs import { ClientType } from "@bitwarden/common/enums/clientType"; import { StateFactory } from "@bitwarden/common/factories/stateFactory"; import { GlobalState } from "@bitwarden/common/models/domain/global-state"; +import { LoginService } from "@bitwarden/common/services/login.service"; import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service"; import { SystemService } from "@bitwarden/common/services/system.service"; import { ElectronCryptoService } from "@bitwarden/electron/services/electronCrypto.service"; @@ -175,6 +177,10 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); EncryptedMessageHandlerService, ], }, + { + provide: LoginServiceAbstraction, + useClass: LoginService, + }, ], }) export class ServicesModule {} diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index c896cd3e26..fd168510b3 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -2021,5 +2021,32 @@ }, "vault": { "message": "Vault" + }, + "loginWithMasterPassword": { + "message": "Log in with master password" + }, + "loggingInAs": { + "message": "Logging in as" + }, + "rememberEmail": { + "message": "Remember email" + }, + "notYou": { + "message": "Not you?" + }, + "newAroundHere": { + "message": "New around here?" + }, + "loggingInTo": { + "message": "Logging in to $DOMAIN$", + "placeholders": { + "domain": { + "content": "$1", + "example": "example.com" + } + } + }, + "logInWithAnotherDevice": { + "message": "Log in with another device" } } diff --git a/apps/desktop/src/scss/misc.scss b/apps/desktop/src/scss/misc.scss index 50ba40ba70..5818b19471 100644 --- a/apps/desktop/src/scss/misc.scss +++ b/apps/desktop/src/scss/misc.scss @@ -310,6 +310,11 @@ form, margin-top: 4px; margin-left: -18px; } + + &.remember-email { + padding-left: 20px; + padding-bottom: 5px; + } } .radio { @@ -482,6 +487,10 @@ app-root > #loading, margin-top: 15px; } +.password-hint-btn { + margin-bottom: 10px; +} + .set-pin-modal { .box { margin-bottom: 15px; diff --git a/apps/desktop/src/scss/pages.scss b/apps/desktop/src/scss/pages.scss index 87862bbef3..f5ca996c44 100644 --- a/apps/desktop/src/scss/pages.scss +++ b/apps/desktop/src/scss/pages.scss @@ -189,6 +189,8 @@ #login-page { flex-direction: column; + justify-content: unset; + padding-top: 20px; .login-header { align-self: flex-start; diff --git a/apps/web/src/app/accounts/hint.component.ts b/apps/web/src/app/accounts/hint.component.ts index f6d3153b03..aaf58fe949 100644 --- a/apps/web/src/app/accounts/hint.component.ts +++ b/apps/web/src/app/accounts/hint.component.ts @@ -5,6 +5,7 @@ import { HintComponent as BaseHintComponent } from "@bitwarden/angular/component import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService } from "@bitwarden/common/abstractions/login.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @Component({ @@ -17,8 +18,9 @@ export class HintComponent extends BaseHintComponent { i18nService: I18nService, apiService: ApiService, platformUtilsService: PlatformUtilsService, - logService: LogService + logService: LogService, + loginService: LoginService ) { - super(router, i18nService, apiService, platformUtilsService, logService); + super(router, i18nService, apiService, platformUtilsService, logService, loginService); } } diff --git a/apps/web/src/app/accounts/login/login.component.html b/apps/web/src/app/accounts/login/login.component.html index 6152b58595..9854403548 100644 --- a/apps/web/src/app/accounts/login/login.component.html +++ b/apps/web/src/app/accounts/login/login.component.html @@ -16,102 +16,122 @@
- - {{ "resetPasswordAutoEnrollInviteWarning" | i18n }} - - -
- - {{ "emailAddress" | i18n }} - - -
- -
- -
-
- + +
+ + {{ "emailAddress" | i18n }} + +
- - {{ "rememberEmail" | i18n }} - -
-
+
+
+ +
+ + {{ "rememberEmail" | i18n }} + +
-
- -
+
+ +
-
- +
- - - {{ "createAccount" | i18n }} - -
- -
- -
- - +

+ {{ "newAroundHere" | i18n }} + {{ "createAccount" | i18n }} +

+
+ + +
+ + {{ "masterPass" | i18n }} + + + + {{ "getMasterPasswordHint" | i18n }} + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + +
+ +
+

{{ "loggingInAs" | i18n }} {{ loggedEmail }}

+ {{ "notYou" | i18n }} +
+
diff --git a/apps/web/src/app/accounts/login/login.component.ts b/apps/web/src/app/accounts/login/login.component.ts index 1c84265049..d461c04c81 100644 --- a/apps/web/src/app/accounts/login/login.component.ts +++ b/apps/web/src/app/accounts/login/login.component.ts @@ -6,12 +6,14 @@ import { first } from "rxjs/operators"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AppIdService } from "@bitwarden/common/abstractions/appId.service"; import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService } from "@bitwarden/common/abstractions/login.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -39,15 +41,16 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest private destroy$ = new Subject(); constructor( + apiService: ApiService, + appIdService: AppIdService, authService: AuthService, router: Router, i18nService: I18nService, - private route: ActivatedRoute, + route: ActivatedRoute, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService, - private apiService: ApiService, private policyApiService: PolicyApiServiceAbstraction, private policyService: InternalPolicyService, logService: LogService, @@ -56,9 +59,12 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest private messagingService: MessagingService, private routerService: RouterService, formBuilder: FormBuilder, - formValidationErrorService: FormValidationErrorsService + formValidationErrorService: FormValidationErrorsService, + loginService: LoginService ) { super( + apiService, + appIdService, authService, router, platformUtilsService, @@ -70,7 +76,9 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest logService, ngZone, formBuilder, - formValidationErrorService + formValidationErrorService, + route, + loginService ); this.onSuccessfulLogin = async () => { this.messagingService.send("setFullWidth"); @@ -82,9 +90,6 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest async ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.queryParams.pipe(first()).subscribe(async (qParams) => { - if (qParams.email != null && qParams.email.indexOf("@") > -1) { - this.formGroup.get("email")?.setValue(qParams.email); - } if (qParams.premium != null) { this.routerService.setPreviousUrl("/settings/premium"); } else if (qParams.org != null) { @@ -102,8 +107,6 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest this.routerService.setPreviousUrl(route.toString()); } await super.ngOnInit(); - const rememberEmail = await this.stateService.getRememberEmail(); - this.formGroup.get("rememberEmail")?.setValue(rememberEmail); }); const invite = await this.stateService.getOrganizationInvitation(); @@ -176,6 +179,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest if (previousUrl) { this.router.navigateByUrl(previousUrl); } else { + this.loginService.clearValues(); this.router.navigate([this.successRoute]); } } diff --git a/apps/web/src/app/accounts/two-factor.component.ts b/apps/web/src/app/accounts/two-factor.component.ts index 5ebf3ea02e..4fb6f6e365 100644 --- a/apps/web/src/app/accounts/two-factor.component.ts +++ b/apps/web/src/app/accounts/two-factor.component.ts @@ -9,6 +9,7 @@ import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService } from "@bitwarden/common/abstractions/login.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { TwoFactorService } from "@bitwarden/common/abstractions/twoFactor.service"; @@ -40,7 +41,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService: LogService, twoFactorService: TwoFactorService, appIdService: AppIdService, - private routerService: RouterService + private routerService: RouterService, + loginService: LoginService ) { super( authService, @@ -54,7 +56,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { route, logService, twoFactorService, - appIdService + appIdService, + loginService ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } @@ -79,6 +82,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { } async goAfterLogIn() { + this.loginService.clearValues(); const previousUrl = this.routerService.getPreviousUrl(); if (previousUrl) { this.router.navigateByUrl(previousUrl); diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index ba22f4422c..ac349561c8 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -13,6 +13,7 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services. import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/abstractions/i18n.service"; +import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/abstractions/login.service"; import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service"; import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -20,6 +21,7 @@ import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/a import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service"; import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/factories/stateFactory"; +import { LoginService } from "@bitwarden/common/services/login.service"; import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service"; import { BroadcasterMessagingService } from "./broadcaster-messaging.service"; @@ -98,6 +100,10 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service"; provide: FileDownloadService, useClass: WebFileDownloadService, }, + { + provide: LoginServiceAbstraction, + useClass: LoginService, + }, ], }) export class CoreModule { diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index ab1fe4f8cf..0edc70d0a6 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -569,12 +569,15 @@ "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, - "loginWithDevice" : { + "loginWithDevice": { "message": "Log in with device" }, "loginWithDeviceEnabledInfo": { "message": "Log in with device must be set up in the settings of the Bitwarden mobile app. Need another option?" }, + "loginWithMasterPassword": { + "message": "Log in with master password" + }, "createAccount": { "message": "Create account" }, @@ -717,7 +720,7 @@ "noOrganizationsList": { "message": "You do not belong to any organizations. Organizations allow you to securely share items with other users." }, - "notificationSentDevice":{ + "notificationSentDevice": { "message": "A notification has been sent to your device." }, "versionNumber": { @@ -5394,6 +5397,12 @@ "numberOfUsers": { "message": "Number of users" }, + "loggingInAs": { + "message": "Logging in as" + }, + "notYou": { + "message": "Not you?" + }, "multiSelectPlaceholder": { "message": "-- Type to Filter --" }, diff --git a/libs/angular/src/components/hint.component.ts b/libs/angular/src/components/hint.component.ts index 7e802722bf..de49e28c86 100644 --- a/libs/angular/src/components/hint.component.ts +++ b/libs/angular/src/components/hint.component.ts @@ -1,12 +1,15 @@ +import { Directive, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService } from "@bitwarden/common/abstractions/login.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PasswordHintRequest } from "@bitwarden/common/models/request/password-hint.request"; -export class HintComponent { +@Directive() +export class HintComponent implements OnInit { email = ""; formPromise: Promise; @@ -18,9 +21,14 @@ export class HintComponent { protected i18nService: I18nService, protected apiService: ApiService, protected platformUtilsService: PlatformUtilsService, - private logService: LogService + private logService: LogService, + private loginService: LoginService ) {} + ngOnInit(): void { + this.email = this.loginService.getEmail() ?? ""; + } + async submit() { if (this.email == null || this.email === "") { this.platformUtilsService.showToast( diff --git a/libs/angular/src/components/login.component.ts b/libs/angular/src/components/login.component.ts index ab0e4df57e..6d4b6966bb 100644 --- a/libs/angular/src/components/login.component.ts +++ b/libs/angular/src/components/login.component.ts @@ -1,8 +1,10 @@ import { Directive, NgZone, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Router } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { take } from "rxjs/operators"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AppIdService } from "@bitwarden/common/abstractions/appId.service"; import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; @@ -12,6 +14,7 @@ import { } from "@bitwarden/common/abstractions/formValidationErrors.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService } from "@bitwarden/common/abstractions/login.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; @@ -29,20 +32,30 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit onSuccessfulLoginNavigate: () => Promise; onSuccessfulLoginTwoFactorNavigate: () => Promise; onSuccessfulLoginForceResetNavigate: () => Promise; - selfHosted = false; + private selfHosted = false; + showLoginWithDevice: boolean; + validatedEmail = false; + paramEmailSet = false; formGroup = this.formBuilder.group({ email: ["", [Validators.required, Validators.email]], masterPassword: ["", [Validators.required, Validators.minLength(8)]], - rememberEmail: [true], + rememberEmail: [false], }); protected twoFactorRoute = "2fa"; protected successRoute = "vault"; protected forcePasswordResetRoute = "update-temp-password"; protected alwaysRememberEmail = false; + protected skipRememberEmail = false; + + get loggedEmail() { + return this.formGroup.value.email; + } constructor( + protected apiService: ApiService, + protected appIdService: AppIdService, protected authService: AuthService, protected router: Router, platformUtilsService: PlatformUtilsService, @@ -54,7 +67,9 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit protected logService: LogService, protected ngZone: NgZone, protected formBuilder: FormBuilder, - protected formValidationErrorService: FormValidationErrorsService + protected formValidationErrorService: FormValidationErrorsService, + protected route: ActivatedRoute, + protected loginService: LoginService ) { super(environmentService, i18nService, platformUtilsService); this.selfHosted = platformUtilsService.isSelfHost(); @@ -65,19 +80,35 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit } async ngOnInit() { - let email = this.formGroup.value.email; + this.route?.queryParams.subscribe((params) => { + if (params != null) { + const queryParamsEmail = params["email"]; + if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) { + this.formGroup.get("email").setValue(queryParamsEmail); + this.paramEmailSet = true; + } + } + }); + let email = this.loginService.getEmail(); + if (email == null || email === "") { email = await this.stateService.getRememberedEmail(); - this.formGroup.get("email")?.setValue(email); + } - if (email == null) { - this.formGroup.get("email")?.setValue(""); - } + if (!this.paramEmailSet) { + this.formGroup.get("email")?.setValue(email ?? ""); } if (!this.alwaysRememberEmail) { - const rememberEmail = (await this.stateService.getRememberedEmail()) != null; + let rememberEmail = this.loginService.getRememberEmail(); + if (rememberEmail == null) { + rememberEmail = (await this.stateService.getRememberedEmail()) != null; + } this.formGroup.get("rememberEmail")?.setValue(rememberEmail); } + + if (email) { + this.validateEmail(); + } } async submit(showToast = true) { @@ -108,6 +139,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit ); this.formPromise = this.authService.logIn(credentials); const response = await this.formPromise; + this.setFormValues(); if (data.rememberEmail || this.alwaysRememberEmail) { await this.stateService.setRememberedEmail(data.email); } else { @@ -130,6 +162,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit } else { const disableFavicon = await this.stateService.getDisableFavicon(); await this.stateService.setDisableFavicon(!!disableFavicon); + this.loginService.clearValues(); if (this.onSuccessfulLogin != null) { this.onSuccessfulLogin(); } @@ -191,6 +224,25 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit ); } + async validateEmail() { + this.formGroup.controls.email.markAsTouched(); + const emailInvalid = this.formGroup.get("email").invalid; + if (!emailInvalid) { + this.toggleValidateEmail(true); + await this.getLoginWithDevice(this.loggedEmail); + } + } + + toggleValidateEmail(value: boolean) { + this.validatedEmail = value; + this.formGroup.controls.masterPassword.reset(); + } + + setFormValues() { + this.loginService.setEmail(this.formGroup.value.email); + this.loginService.setRememberEmail(this.formGroup.value.rememberEmail); + } + private getErrorToastMessage() { const error: AllValidationErrors = this.formValidationErrorService .getFormValidationErrors(this.formGroup.controls) @@ -213,8 +265,19 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit return `${error.controlName}${name}`; } + private async getLoginWithDevice(email: string) { + try { + const deviceIdentifier = await this.appIdService.getAppId(); + const res = await this.apiService.getKnownDevice(email, deviceIdentifier); + //ensure the application is not self-hosted + this.showLoginWithDevice = res && !this.selfHosted; + } catch (e) { + this.showLoginWithDevice = false; + } + } + protected focusInput() { - const email = this.formGroup.value.email; + const email = this.loggedEmail; document.getElementById(email == null || email === "" ? "email" : "masterPassword").focus(); } } diff --git a/libs/angular/src/components/two-factor.component.ts b/libs/angular/src/components/two-factor.component.ts index 1c2f4ff60b..f61e54710b 100644 --- a/libs/angular/src/components/two-factor.component.ts +++ b/libs/angular/src/components/two-factor.component.ts @@ -9,6 +9,7 @@ import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService } from "@bitwarden/common/abstractions/login.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { TwoFactorService } from "@bitwarden/common/abstractions/twoFactor.service"; @@ -59,7 +60,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected route: ActivatedRoute, protected logService: LogService, protected twoFactorService: TwoFactorService, - protected appIdService: AppIdService + protected appIdService: AppIdService, + protected loginService: LoginService ) { super(environmentService, i18nService, platformUtilsService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); @@ -204,6 +206,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI return; } if (this.onSuccessfulLogin != null) { + this.loginService.clearValues(); this.onSuccessfulLogin(); } if (response.resetMasterPassword) { @@ -213,8 +216,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.successRoute = "update-temp-password"; } if (this.onSuccessfulLoginNavigate != null) { + this.loginService.clearValues(); this.onSuccessfulLoginNavigate(); } else { + this.loginService.clearValues(); this.router.navigate([this.successRoute], { queryParams: { identifier: this.identifier, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 83725838a5..5f49041bd7 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -31,6 +31,7 @@ import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/abstractions/i18n.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/abstractions/keyConnector.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; +import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/abstractions/login.service"; import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; @@ -88,6 +89,7 @@ import { FolderApiService } from "@bitwarden/common/services/folder/folder-api.s import { FolderService } from "@bitwarden/common/services/folder/folder.service"; import { FormValidationErrorsService } from "@bitwarden/common/services/formValidationErrors.service"; import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service"; +import { LoginService } from "@bitwarden/common/services/login.service"; import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { OrganizationApiService } from "@bitwarden/common/services/organization/organization-api.service"; import { OrganizationService } from "@bitwarden/common/services/organization/organization.service"; @@ -578,6 +580,10 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; useClass: ValidationService, deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], }, + { + provide: LoginServiceAbstraction, + useClass: LoginService, + }, ], }) export class JslibServicesModule {} diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 14dc26cc59..74f9edebd6 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -479,6 +479,7 @@ export abstract class ApiService { putDeviceVerificationSettings: ( request: DeviceVerificationRequest ) => Promise; + getKnownDevice: (email: string, deviceIdentifier: string) => Promise; getEmergencyAccessTrusted: () => Promise>; getEmergencyAccessGranted: () => Promise>; diff --git a/libs/common/src/abstractions/login.service.ts b/libs/common/src/abstractions/login.service.ts new file mode 100644 index 0000000000..0823611546 --- /dev/null +++ b/libs/common/src/abstractions/login.service.ts @@ -0,0 +1,7 @@ +export abstract class LoginService { + getEmail: () => string; + getRememberEmail: () => boolean; + setEmail: (value: string) => void; + setRememberEmail: (value: boolean) => void; + clearValues: () => void; +} diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index bab17eb43f..7c39958bce 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -1518,6 +1518,12 @@ export class ApiService implements ApiServiceAbstraction { return new DeviceVerificationResponse(r); } + async getKnownDevice(email: string, deviceIdentifier: string): Promise { + const path = `/devices/knowndevice/${email}/${deviceIdentifier}`; + const r = await this.send("GET", path, null, false, true); + return r as boolean; + } + // Emergency Access APIs async getEmergencyAccessTrusted(): Promise> { diff --git a/libs/common/src/services/login.service.ts b/libs/common/src/services/login.service.ts new file mode 100644 index 0000000000..8a06f6b7a0 --- /dev/null +++ b/libs/common/src/services/login.service.ts @@ -0,0 +1,27 @@ +import { LoginService as LoginServiceAbstraction } from "../abstractions/login.service"; + +export class LoginService implements LoginServiceAbstraction { + private _email: string; + private _rememberEmail: boolean; + + getEmail() { + return this._email; + } + + getRememberEmail() { + return this._rememberEmail; + } + + setEmail(value: string) { + this._email = value; + } + + setRememberEmail(value: boolean) { + this._rememberEmail = value; + } + + clearValues() { + this._email = null; + this._rememberEmail = null; + } +}