1
0
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:
Todd Martin
2022-10-28 14:54:55 -04:00
committed by GitHub
parent aa256b8a70
commit 2cd65939d5
38 changed files with 703 additions and 269 deletions

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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);
};
}

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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 {}

View File

@@ -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"
}
}

View File

@@ -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;

View File

@@ -189,6 +189,8 @@
#login-page {
flex-direction: column;
justify-content: unset;
padding-top: 20px;
.login-header {
align-self: flex-start;