mirror of
https://github.com/bitwarden/browser
synced 2025-12-20 10:13:31 +00:00
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 <gsmithwalter@gmail.com> * [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 <gsmith@bitwarden.com> Co-authored-by: Addison Beck <addisonbeck1@gmail.com> Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com> Co-authored-by: gbubemismith <gsmithwalter@gmail.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,78 +22,135 @@
|
||||
<div id="content" class="content">
|
||||
<img class="logo-image" alt="Bitwarden" />
|
||||
<p class="lead">{{ "loginOrCreateNewAccount" | i18n }}</p>
|
||||
<div class="box last">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="email">{{ "emailAddress" | i18n }}</label>
|
||||
<input id="email" type="email" formControlName="email" appInputVerbatim="false" />
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<!-- start email -->
|
||||
<ng-container *ngIf="!validatedEmail; else loginPage">
|
||||
<div class="box last">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="email">{{ "emailAddress" | i18n }}</label>
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
class="monospaced"
|
||||
formControlName="masterPassword"
|
||||
appInputVerbatim
|
||||
id="email"
|
||||
type="email"
|
||||
formControlName="email"
|
||||
appInputVerbatim="false"
|
||||
(keyup.enter)="validateEmail()"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
</div>
|
||||
<div class="box-footer" *ngIf="selfHostedDomain">
|
||||
{{ "loggingInTo" | i18n: selfHostedDomain }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkbox remember-email">
|
||||
<label for="rememberEmail">
|
||||
<input
|
||||
id="rememberEmail"
|
||||
type="checkbox"
|
||||
name="rememberEmail"
|
||||
formControlName="rememberEmail"
|
||||
/>
|
||||
{{ "rememberEmail" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="buttons with-rows">
|
||||
<div class="buttons-row">
|
||||
<button type="button" class="btn primary block" (click)="continue()">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<p class="no-margin">{{ "newAroundHere" | i18n }}</p>
|
||||
<button type="button" class="text text-primary" routerLink="/register">
|
||||
{{ "createAccount" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-template [formGroup]="formGroup" #loginPage>
|
||||
<div class="box last">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||
<div class="row-main">
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
class="monospaced"
|
||||
formControlName="masterPassword"
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
[attr.aria-pressed]="showPassword"
|
||||
(click)="togglePassword()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box last" [hidden]="!showCaptcha()">
|
||||
<div class="box-content">
|
||||
<iframe id="hcaptcha_iframe" style="margin-top: 20px"></iframe>
|
||||
<div class="box-content-row">
|
||||
<button
|
||||
class="btn block"
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
[attr.aria-pressed]="showPassword"
|
||||
(click)="togglePassword()"
|
||||
routerLink="/accessibility-cookie"
|
||||
(click)="setFormValues()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
<i class="bwi bwi-universal-access" aria-hidden="true"></i>
|
||||
{{ "loadAccessibilityCookie" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box last" [hidden]="!showCaptcha()">
|
||||
<div class="box-content">
|
||||
<iframe id="hcaptcha_iframe" style="margin-top: 20px"></iframe>
|
||||
<div class="box-content-row">
|
||||
<button class="btn block" type="button" routerLink="/accessibility-cookie">
|
||||
<i class="bwi bwi-universal-access" aria-hidden="true"></i>
|
||||
{{ "loadAccessibilityCookie" | i18n }}
|
||||
<div class="buttons with-rows">
|
||||
<div class="buttons-row">
|
||||
<button type="submit" class="btn primary block" [disabled]="form.loading">
|
||||
<b [hidden]="form.loading"
|
||||
><i class="bwi bwi-sign-in" aria-hidden="true"></i>
|
||||
{{ "loginWithMasterPassword" | i18n }}</b
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons-row">
|
||||
<button
|
||||
type="button"
|
||||
(click)="launchSsoBrowser('desktop', 'bitwarden://sso-callback')"
|
||||
class="btn block"
|
||||
>
|
||||
<i class="bwi bwi-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons with-rows">
|
||||
<div class="buttons-row">
|
||||
<button type="submit" class="btn primary block" [disabled]="form.loading">
|
||||
<b [hidden]="form.loading"
|
||||
><i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "logIn" | i18n }}</b
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" routerLink="/register" class="btn block">
|
||||
<i class="bwi bwi-pencil-square" aria-hidden="true"></i> {{ "createAccount" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons-row">
|
||||
<div class="sub-options">
|
||||
<button
|
||||
type="button"
|
||||
(click)="launchSsoBrowser('desktop', 'bitwarden://sso-callback')"
|
||||
class="btn block"
|
||||
class="text text-primary password-hint-btn"
|
||||
routerLink="/hint"
|
||||
(click)="setFormValues()"
|
||||
>
|
||||
<i class="bwi bwi-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
|
||||
{{ "getMasterPasswordHint" | i18n }}
|
||||
</button>
|
||||
<div>
|
||||
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ loggedEmail }}</p>
|
||||
<a [routerLink]="[]" (click)="toggleValidateEmail(false)">{{ "notYou" | i18n }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<button type="button" routerLink="/hint">{{ "getMasterPasswordHint" | i18n }}</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
<ng-container *ngIf="activeAccount?.email != null">
|
||||
<div class="border" *ngIf="numberOfAccounts > 0"></div>
|
||||
<ng-container *ngIf="numberOfAccounts < 4">
|
||||
<button type="button" class="add" routerLink="/login" (click)="addAccount()">
|
||||
<button type="button" class="add" (click)="addAccount()">
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -189,6 +189,8 @@
|
||||
|
||||
#login-page {
|
||||
flex-direction: column;
|
||||
justify-content: unset;
|
||||
padding-top: 20px;
|
||||
|
||||
.login-header {
|
||||
align-self: flex-start;
|
||||
|
||||
Reference in New Issue
Block a user