1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-29 12:55:21 +01: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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 703 additions and 269 deletions

View File

@ -2028,5 +2028,20 @@
"example": "Jun 15, 2015" "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"
} }
} }

View File

@ -1,7 +1,9 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header> <header>
<div class="left"> <div class="left">
<button type="button" routerLink="/login">{{ "cancel" | i18n }}</button> <button type="button" routerLink="/login">
{{ "cancel" | i18n }}
</button>
</div> </div>
<h1 class="center"> <h1 class="center">
<span class="title">{{ "passwordHint" | i18n }}</span> <span class="title">{{ "passwordHint" | i18n }}</span>

View File

@ -1,10 +1,11 @@
import { Component } from "@angular/core"; 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 { HintComponent as BaseHintComponent } from "@bitwarden/angular/components/hint.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@Component({ @Component({
@ -17,8 +18,14 @@ export class HintComponent extends BaseHintComponent {
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
i18nService: I18nService, i18nService: I18nService,
apiService: ApiService, 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]);
};
} }
} }

View File

@ -2,15 +2,28 @@
<div class="content"> <div class="content">
<div class="logo-image"></div> <div class="logo-image"></div>
<p class="lead text-center">{{ "loginOrCreateNewAccount" | i18n }}</p> <p class="lead text-center">{{ "loginOrCreateNewAccount" | i18n }}</p>
<button type="button" class="btn primary block" routerLink="/login"> <form #form [formGroup]="formGroup" (ngSubmit)="submit()">
<b>{{ "login" | i18n }}</b> <div class="box">
</button> <div class="box-content">
<button type="button" (click)="launchSsoBrowser()" class="btn block"> <div class="box-content-row" appBoxRow>
<i class="bwi bwi-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }} <label for="email">{{ "emailAddress" | i18n }}</label>
</button> <input id="email" type="email" formControlName="email" appInputVerbatim="false" />
<button type="button" class="btn block" routerLink="/register"> </div>
{{ "createAccount" | i18n }} </div>
</button> <div class="box-footer no-margin" *ngIf="selfHostedDomain">
{{ "loggingInTo" | i18n: selfHostedDomain }}
</div>
</div>
<div class="box">
<button type="submit" class="btn primary block">
<b>{{ "continue" | i18n }}</b>
</button>
</div>
</form>
<p class="createAccountLink">
{{ "newAroundHere" | i18n }}
<a routerLink="/register">{{ "createAccount" | i18n }}</a>
</p>
</div> </div>
</div> </div>
<button type="button" routerLink="/environment" class="settings-icon"> <button type="button" routerLink="/environment" class="settings-icon">

View File

@ -1,63 +1,56 @@
import { Component } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { Utils } from "@bitwarden/common/misc/utils";
@Component({ @Component({
selector: "app-home", selector: "app-home",
templateUrl: "home.component.html", templateUrl: "home.component.html",
}) })
export class HomeComponent { export class HomeComponent implements OnInit {
loginInitiated = false;
formGroup = this.formBuilder.group({
email: ["", [Validators.required, Validators.email]],
});
constructor( constructor(
protected platformUtilsService: PlatformUtilsService, protected platformUtilsService: PlatformUtilsService,
private passwordGenerationService: PasswordGenerationService,
private stateService: StateService, private stateService: StateService,
private cryptoFunctionService: CryptoFunctionService, private formBuilder: FormBuilder,
private environmentService: EnvironmentService private router: Router,
private i18nService: I18nService,
private environmentService: EnvironmentService,
private route: ActivatedRoute
) {} ) {}
async ngOnInit(): Promise<void> {
const rememberedEmail = await this.stateService.getRememberedEmail();
if (rememberedEmail != null) {
this.formGroup.patchValue({ email: await this.stateService.getRememberedEmail() });
}
}
async launchSsoBrowser() { submit() {
// Generate necessary sso params this.formGroup.markAllAsTouched();
const passwordOptions: any = { if (this.formGroup.invalid) {
type: "password", this.platformUtilsService.showToast(
length: 64, "error",
uppercase: true, this.i18nService.t("errorOccured"),
lowercase: true, this.i18nService.t("invalidEmail")
numbers: true, );
special: false, return;
};
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"; this.stateService.setRememberedEmail(this.formGroup.value.email);
// Launch browser this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } });
this.platformUtilsService.launchUri( }
url +
"/#/sso?clientId=browser" + get selfHostedDomain() {
"&redirectUri=" + return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null;
encodeURIComponent(redirectUri) +
"&state=" +
state +
"&codeChallenge=" +
codeChallenge
);
} }
} }

View File

@ -1,25 +1,12 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" [formGroup]="formGroup"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise" [formGroup]="formGroup">
<header> <header>
<div class="left"> <h1 class="login-center">
<button type="button" routerLink="/home">{{ "cancel" | i18n }}</button> <span class="title">{{ "logIn" | i18n }}</span>
</div>
<h1 class="center">
<span class="title">{{ "appName" | i18n }}</span>
</h1> </h1>
<div class="right">
<button type="submit" [disabled]="form.loading">
<span [hidden]="form.loading">{{ "login" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header> </header>
<main tabindex="-1"> <main tabindex="-1">
<div class="box"> <div class="box">
<div class="box-content"> <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="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main"> <div class="row-main">
<label for="masterPassword">{{ "masterPass" | i18n }}</label> <label for="masterPassword">{{ "masterPass" | i18n }}</label>
@ -52,13 +39,27 @@
<iframe id="hcaptcha_iframe" height="80"></iframe> <iframe id="hcaptcha_iframe" height="80"></iframe>
</div> </div>
</div> </div>
<div class="box-footer">
<button type="button" class="btn link" routerLink="/hint" (click)="setFormValues()">
<b>{{ "getMasterPasswordHint" | i18n }}</b>
</button>
</div>
</div> </div>
<p class="text-center text-muted" *ngIf="selfHostedDomain">
{{ "loggingInTo" | i18n: selfHostedDomain }}
</p>
<p class="text-center">
<button type="button" routerLink="/hint">{{ "getMasterPasswordHint" | i18n }}</button>
</p>
<app-private-mode-warning></app-private-mode-warning> <app-private-mode-warning></app-private-mode-warning>
<div class="content login-buttons">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<span [hidden]="form.loading"
><b>{{ "logInWithMasterPassword" | i18n }}</b></span
>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
<button type="button" (click)="launchSsoBrowser()" class="btn block">
<i class="bwi bwi-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
</button>
<div class="small">
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ loggedEmail }}</p>
<a routerLink="/home">{{ "notYou" | i18n }}</a>
</div>
</div>
</main> </main>
</form> </form>

View File

@ -1,27 +1,33 @@
import { Component, NgZone } from "@angular/core"; import { Component, NgZone } from "@angular/core";
import { FormBuilder } from "@angular/forms"; 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 { 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 { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service"; import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service"; import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { Utils } from "@bitwarden/common/misc/utils";
@Component({ @Component({
selector: "app-login", selector: "app-login",
templateUrl: "login.component.html", templateUrl: "login.component.html",
}) })
export class LoginComponent extends BaseLoginComponent { export class LoginComponent extends BaseLoginComponent {
protected alwaysRememberEmail = true; protected skipRememberEmail = true;
constructor( constructor(
apiService: ApiService,
appIdService: AppIdService,
authService: AuthService, authService: AuthService,
router: Router, router: Router,
protected platformUtilsService: PlatformUtilsService, protected platformUtilsService: PlatformUtilsService,
@ -34,9 +40,13 @@ export class LoginComponent extends BaseLoginComponent {
logService: LogService, logService: LogService,
ngZone: NgZone, ngZone: NgZone,
formBuilder: FormBuilder, formBuilder: FormBuilder,
formValidationErrorService: FormValidationErrorsService formValidationErrorService: FormValidationErrorsService,
route: ActivatedRoute,
loginService: LoginService
) { ) {
super( super(
apiService,
appIdService,
authService, authService,
router, router,
platformUtilsService, platformUtilsService,
@ -48,7 +58,9 @@ export class LoginComponent extends BaseLoginComponent {
logService, logService,
ngZone, ngZone,
formBuilder, formBuilder,
formValidationErrorService formValidationErrorService,
route,
loginService
); );
super.onSuccessfulLogin = async () => { super.onSuccessfulLogin = async () => {
await syncService.fullSync(true); await syncService.fullSync(true);
@ -59,4 +71,45 @@ export class LoginComponent extends BaseLoginComponent {
settings() { settings() {
this.router.navigate(["environment"]); 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
);
}
} }

View File

@ -10,6 +10,7 @@ import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.s
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
@ -44,7 +45,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
private messagingService: MessagingService, private messagingService: MessagingService,
logService: LogService, logService: LogService,
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
appIdService: AppIdService appIdService: AppIdService,
loginService: LoginService
) { ) {
super( super(
authService, authService,
@ -58,9 +60,11 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
route, route,
logService, logService,
twoFactorService, twoFactorService,
appIdService appIdService,
loginService
); );
super.onSuccessfulLogin = () => { super.onSuccessfulLogin = () => {
this.loginService.clearValues();
return syncService.fullSync(true); return syncService.fullSync(true);
}; };
super.successRoute = "/tabs/vault"; super.successRoute = "/tabs/vault";

View File

@ -1,4 +1,4 @@
<app-callout type="warning" *ngIf="showWarning"> <app-callout class="app-private-mode-warning" type="warning" *ngIf="showWarning">
{{ "privateModeWarning" | i18n }} {{ "privateModeWarning" | i18n }}
<a href="https://bitwarden.com/help/article/private-mode/" target="_blank" rel="noopener">{{ <a href="https://bitwarden.com/help/article/private-mode/" target="_blank" rel="noopener">{{
"learnMore" | i18n "learnMore" | i18n

View File

@ -174,6 +174,11 @@ header {
.right { .right {
justify-content: flex-end; justify-content: flex-end;
align-items: center;
app-avatar {
max-height: 30px;
margin-right: 5px;
}
} }
.center { .center {
@ -183,6 +188,10 @@ header {
min-width: 0; min-width: 0;
} }
.login-center {
margin: auto;
}
app-pop-out > button, app-pop-out > button,
div > button, div > button,
div > a { div > a {

View File

@ -83,6 +83,11 @@
margin: 5px 10px; margin: 5px 10px;
font-size: $font-size-small; font-size: $font-size-small;
button.btn {
font-size: $font-size-small;
padding: 0;
}
@include themify($themes) { @include themify($themes) {
color: themed("mutedColor"); color: themed("mutedColor");
} }

View File

@ -440,3 +440,7 @@ app-vault-view .box-footer {
html.force_redraw { html.force_redraw {
animation: redraw 1s linear infinite; animation: redraw 1s linear infinite;
} }
.rounded-circle {
border-radius: 50% !important;
}

View File

@ -88,7 +88,7 @@ app-home {
} }
} }
app-private-mode-warning { .app-private-mode-warning {
display: block; display: block;
padding-top: 1rem; padding-top: 1rem;
} }
@ -115,3 +115,11 @@ body.body-full {
} }
} }
} }
.createAccountLink {
padding-top: 30px;
}
.login-buttons > button {
margin: 15px 0 15px 0;
}

View File

@ -24,6 +24,7 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service"; import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { LogService as LogServiceAbstraction } from "@bitwarden/common/abstractions/log.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 { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; 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 { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { AuthService } from "@bitwarden/common/services/auth.service"; import { AuthService } from "@bitwarden/common/services/auth.service";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.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 { SearchService } from "@bitwarden/common/services/search.service";
import MainBackground from "../../background/main.background"; import MainBackground from "../../background/main.background";
@ -309,6 +311,10 @@ function getBgService<T>(service: keyof MainBackground) {
provide: FileDownloadService, provide: FileDownloadService,
useClass: BrowserFileDownloadService, useClass: BrowserFileDownloadService,
}, },
{
provide: LoginServiceAbstraction,
useClass: LoginService,
},
{ {
provide: AbstractThemingService, provide: AbstractThemingService,
useFactory: () => { useFactory: () => {

View File

@ -5,6 +5,7 @@ import { HintComponent as BaseHintComponent } from "@bitwarden/angular/component
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@Component({ @Component({
@ -17,8 +18,9 @@ export class HintComponent extends BaseHintComponent {
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
i18nService: I18nService, i18nService: I18nService,
apiService: ApiService, 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"> <div id="content" class="content">
<img class="logo-image" alt="Bitwarden" /> <img class="logo-image" alt="Bitwarden" />
<p class="lead">{{ "loginOrCreateNewAccount" | i18n }}</p> <p class="lead">{{ "loginOrCreateNewAccount" | i18n }}</p>
<div class="box last"> <!-- start email -->
<div class="box-content"> <ng-container *ngIf="!validatedEmail; else loginPage">
<div class="box-content-row" appBoxRow> <div class="box last">
<label for="email">{{ "emailAddress" | i18n }}</label> <div class="box-content">
<input id="email" type="email" formControlName="email" appInputVerbatim="false" /> <div class="box-content-row" appBoxRow>
</div> <label for="email">{{ "emailAddress" | i18n }}</label>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input <input
id="masterPassword" id="email"
type="{{ showPassword ? 'text' : 'password' }}" type="email"
class="monospaced" formControlName="email"
formControlName="masterPassword" appInputVerbatim="false"
appInputVerbatim (keyup.enter)="validateEmail()"
/> />
</div> </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 <button
class="btn block"
type="button" type="button"
class="row-btn" routerLink="/accessibility-cookie"
appStopClick (click)="setFormValues()"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword()"
> >
<i <i class="bwi bwi-universal-access" aria-hidden="true"></i>
class="bwi bwi-lg" {{ "loadAccessibilityCookie" | i18n }}
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="buttons with-rows">
<div class="box last" [hidden]="!showCaptcha()"> <div class="buttons-row">
<div class="box-content"> <button type="submit" class="btn primary block" [disabled]="form.loading">
<iframe id="hcaptcha_iframe" style="margin-top: 20px"></iframe> <b [hidden]="form.loading"
<div class="box-content-row"> ><i class="bwi bwi-sign-in" aria-hidden="true"></i>
<button class="btn block" type="button" routerLink="/accessibility-cookie"> {{ "loginWithMasterPassword" | i18n }}</b
<i class="bwi bwi-universal-access" aria-hidden="true"></i> >
{{ "loadAccessibilityCookie" | i18n }} <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> </button>
</div> </div>
</div> </div>
</div> <div class="sub-options">
<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">
<button <button
type="button" type="button"
(click)="launchSsoBrowser('desktop', 'bitwarden://sso-callback')" class="text text-primary password-hint-btn"
class="btn block" routerLink="/hint"
(click)="setFormValues()"
> >
<i class="bwi bwi-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }} {{ "getMasterPasswordHint" | i18n }}
</button> </button>
<div>
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ loggedEmail }}</p>
<a [routerLink]="[]" (click)="toggleValidateEmail(false)">{{ "notYou" | i18n }}</a>
</div>
</div> </div>
</div> </ng-template>
<div class="sub-options">
<button type="button" routerLink="/hint">{{ "getMasterPasswordHint" | i18n }}</button>
</div>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,9 +1,11 @@
import { Component, NgZone, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core"; import { Component, NgZone, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core";
import { FormBuilder } from "@angular/forms"; 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 { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component";
import { ModalService } from "@bitwarden/angular/services/modal.service"; 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 { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.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 { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.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 }) @ViewChild("environment", { read: ViewContainerRef, static: true })
environmentModal: ViewContainerRef; environmentModal: ViewContainerRef;
showingModal = false; webVaultHostname = "";
protected alwaysRememberEmail = true; showingModal = false;
private deferFocus: boolean = null; private deferFocus: boolean = null;
get loggedEmail() {
return this.formGroup.value.email;
}
get selfHostedDomain() {
return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null;
}
constructor( constructor(
apiService: ApiService,
appIdService: AppIdService,
authService: AuthService, authService: AuthService,
router: Router, router: Router,
i18nService: I18nService, i18nService: I18nService,
@ -51,9 +64,13 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
private messagingService: MessagingService, private messagingService: MessagingService,
logService: LogService, logService: LogService,
formBuilder: FormBuilder, formBuilder: FormBuilder,
formValidationErrorService: FormValidationErrorsService formValidationErrorService: FormValidationErrorsService,
route: ActivatedRoute,
loginService: LoginService
) { ) {
super( super(
apiService,
appIdService,
authService, authService,
router, router,
platformUtilsService, platformUtilsService,
@ -65,7 +82,9 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
logService, logService,
ngZone, ngZone,
formBuilder, formBuilder,
formValidationErrorService formValidationErrorService,
route,
loginService
); );
super.onSuccessfulLogin = () => { super.onSuccessfulLogin = () => {
return syncService.fullSync(true); return syncService.fullSync(true);
@ -127,7 +146,23 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
this.showPassword = false; 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() { async submit() {
if (!this.validatedEmail) {
return;
}
await super.submit(); await super.submit();
if (this.captchaSiteKey) { if (this.captchaSiteKey) {
const content = document.getElementById("content") as HTMLDivElement; 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 { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
@ -41,7 +42,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
route: ActivatedRoute, route: ActivatedRoute,
logService: LogService, logService: LogService,
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
appIdService: AppIdService appIdService: AppIdService,
loginService: LoginService
) { ) {
super( super(
authService, authService,
@ -55,9 +57,11 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
route, route,
logService, logService,
twoFactorService, twoFactorService,
appIdService appIdService,
loginService
); );
super.onSuccessfulLogin = () => { super.onSuccessfulLogin = () => {
this.loginService.clearValues();
return syncService.fullSync(true); return syncService.fullSync(true);
}; };
} }

View File

@ -90,7 +90,7 @@
<ng-container *ngIf="activeAccount?.email != null"> <ng-container *ngIf="activeAccount?.email != null">
<div class="border" *ngIf="numberOfAccounts > 0"></div> <div class="border" *ngIf="numberOfAccounts > 0"></div>
<ng-container *ngIf="numberOfAccounts < 4"> <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 }} <i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
</button> </button>
</ng-container> </ng-container>

View File

@ -1,6 +1,7 @@
import { animate, state, style, transition, trigger } from "@angular/animations"; import { animate, state, style, transition, trigger } from "@angular/animations";
import { ConnectedPosition } from "@angular/cdk/overlay"; import { ConnectedPosition } from "@angular/cdk/overlay";
import { Component, OnDestroy, OnInit } from "@angular/core"; import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { concatMap, Subject, takeUntil } from "rxjs"; import { concatMap, Subject, takeUntil } from "rxjs";
import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/abstractions/auth.service";
@ -91,6 +92,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
private stateService: StateService, private stateService: StateService,
private authService: AuthService, private authService: AuthService,
private messagingService: MessagingService, private messagingService: MessagingService,
private router: Router,
private tokenService: TokenService private tokenService: TokenService
) {} ) {}
@ -142,6 +144,8 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
async addAccount() { async addAccount() {
this.close(); this.close();
await this.stateService.setActiveUser(null); await this.stateService.setActiveUser(null);
await this.stateService.setRememberedEmail(null);
this.router.navigate(["/login"]);
} }
private async createSwitcherAccounts(baseAccounts: { private async createSwitcherAccounts(baseAccounts: {

View File

@ -23,6 +23,7 @@ import {
LogService, LogService,
LogService as LogServiceAbstraction, LogService as LogServiceAbstraction,
} from "@bitwarden/common/abstractions/log.service"; } 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 { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.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 { ClientType } from "@bitwarden/common/enums/clientType";
import { StateFactory } from "@bitwarden/common/factories/stateFactory"; import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { GlobalState } from "@bitwarden/common/models/domain/global-state"; 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 { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { SystemService } from "@bitwarden/common/services/system.service"; import { SystemService } from "@bitwarden/common/services/system.service";
import { ElectronCryptoService } from "@bitwarden/electron/services/electronCrypto.service"; import { ElectronCryptoService } from "@bitwarden/electron/services/electronCrypto.service";
@ -175,6 +177,10 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK");
EncryptedMessageHandlerService, EncryptedMessageHandlerService,
], ],
}, },
{
provide: LoginServiceAbstraction,
useClass: LoginService,
},
], ],
}) })
export class ServicesModule {} export class ServicesModule {}

View File

@ -2021,5 +2021,32 @@
}, },
"vault": { "vault": {
"message": "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-top: 4px;
margin-left: -18px; margin-left: -18px;
} }
&.remember-email {
padding-left: 20px;
padding-bottom: 5px;
}
} }
.radio { .radio {
@ -482,6 +487,10 @@ app-root > #loading,
margin-top: 15px; margin-top: 15px;
} }
.password-hint-btn {
margin-bottom: 10px;
}
.set-pin-modal { .set-pin-modal {
.box { .box {
margin-bottom: 15px; margin-bottom: 15px;

View File

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

View File

@ -5,6 +5,7 @@ import { HintComponent as BaseHintComponent } from "@bitwarden/angular/component
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@Component({ @Component({
@ -17,8 +18,9 @@ export class HintComponent extends BaseHintComponent {
i18nService: I18nService, i18nService: I18nService,
apiService: ApiService, apiService: ApiService,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
logService: LogService logService: LogService,
loginService: LoginService
) { ) {
super(router, i18nService, apiService, platformUtilsService, logService); super(router, i18nService, apiService, platformUtilsService, logService, loginService);
} }
} }

View File

@ -16,102 +16,122 @@
<div <div
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6" class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
> >
<bit-callout <ng-container *ngIf="!validatedEmail; else loginPage">
type="warning" <div class="tw-mb-3">
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}" <bit-form-field>
*ngIf="showResetPasswordAutoEnrollWarning" <bit-label>{{ "emailAddress" | i18n }}</bit-label>
> <input
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }} id="login_input_email"
</bit-callout> bitInput
type="email"
<div class="tw-mb-3"> formControlName="email"
<bit-form-field> (keyup.enter)="validateEmail()"
<bit-label>{{ "emailAddress" | i18n }}</bit-label> />
<input id="login_input_email" bitInput type="email" formControlName="email" /> </bit-form-field>
</bit-form-field>
</div>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
id="login_input_master-password"
bitInput
type="{{ showPassword ? 'text' : 'password' }}"
formControlName="masterPassword"
/>
<button type="button" bitSuffix bitButton (click)="togglePassword()">
<i
aria-hidden="true"
class="bwi bwi-lg bwi-eye"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<bit-hint>
<a routerLink="/hint">{{ "getMasterPasswordHint" | i18n }}</a>
</bit-hint>
</bit-form-field>
</div>
<div class="tw-mb-3 tw-flex tw-items-start">
<div class="tw-flex tw-h-6 tw-items-center">
<input
id="login_input_remember-email"
class="tw-w-4 tw-rounded tw-border"
bitInput
type="checkbox"
formControlName="rememberEmail"
/>
</div> </div>
<bit-label class="ml-2">
{{ "rememberEmail" | i18n }}
</bit-label>
</div>
<hr /> <div class="tw-mb-3 tw-flex tw-items-start">
<div class="tw-flex tw-h-6 tw-items-center">
<input
id="login_input_remember-email"
class="tw-w-4 tw-rounded tw-border"
bitInput
type="checkbox"
formControlName="rememberEmail"
/>
</div>
<bit-label class="ml-2">
{{ "rememberEmail" | i18n }}
</bit-label>
</div>
<div [hidden]="!showCaptcha()"> <div class="tw-mb-3">
<iframe id="hcaptcha_iframe" height="80"></iframe> <button
</div> bitButton
type="button"
buttonType="primary"
class="tw-w-full"
[disabled]="form.loading"
(click)="validateEmail()"
>
<span> {{ "continue" | i18n }} </span>
</button>
</div>
<div class="tw-mb-3 tw-flex tw-space-x-4"> <hr />
<button
bitButton
buttonType="primary"
type="submit"
[block]="true"
[loading]="form.loading"
[disabled]="form.loading"
>
<span> <i class="bwi bwi-sign-in"></i> {{ "logIn" | i18n }} </span>
</button>
<a bitButton buttonType="secondary" routerLink="/register" [block]="true"> <p class="tw-m-0 tw-text-sm">
<i class="bwi bwi-pencil-square"></i> {{ "newAroundHere" | i18n }}
{{ "createAccount" | i18n }} <a routerLink="/register">{{ "createAccount" | i18n }}</a>
</a> </p>
</div> </ng-container>
<div class="tw-mb-3" *ngIf="!selfHosted && showPasswordless">
<button
bitButton
type="button"
buttonType="secondary"
class="tw-w-full"
(click)="startPasswordlessLogin()"
[disabled]="form.loading"
>
<span> <i class="bwi bwi-mobile"></i> {{ "loginWithDevice" | i18n }} </span>
</button>
</div>
<div class="tw-mb-3">
<a routerLink="/sso" bitButton buttonType="secondary" class="tw-w-full">
<i class="bwi bwi-provider tw-mr-2"></i>
{{ "enterpriseSingleSignOn" | i18n }}
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
</form> </form>
<ng-template [formGroup]="formGroup" #loginPage>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
id="login_input_master-password"
bitInput
type="{{ showPassword ? 'text' : 'password' }}"
formControlName="masterPassword"
/>
<button type="button" bitSuffix bitButton (click)="togglePassword()">
<i
aria-hidden="true"
class="bwi bwi-lg bwi-eye"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<bit-hint>
<a routerLink="/hint" (click)="setFormValues()">{{ "getMasterPasswordHint" | i18n }}</a>
</bit-hint>
</bit-form-field>
</div>
<div [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
<div class="tw-mb-3 tw-flex tw-space-x-4">
<button bitButton buttonType="primary" type="submit" [block]="true" [loading]="form.loading">
<span> {{ "loginWithMasterPassword" | i18n }} </span>
</button>
</div>
<div class="tw-mb-3" *ngIf="showLoginWithDevice && showPasswordless">
<button
bitButton
type="button"
[block]="true"
buttonType="secondary"
(click)="startPasswordlessLogin()"
>
<span> <i class="bwi bwi-mobile"></i> {{ "loginWithDevice" | i18n }} </span>
</button>
</div>
<div class="tw-mb-3">
<a
routerLink="/sso"
(click)="setFormValues()"
bitButton
buttonType="secondary"
class="tw-w-full"
>
<i class="bwi bwi-provider tw-mr-2"></i>
{{ "enterpriseSingleSignOn" | i18n }}
</a>
</div>
<hr />
<div class="tw-m-0 tw-text-sm">
<p class="tw-mb-1">{{ "loggingInAs" | i18n }} {{ loggedEmail }}</p>
<a [routerLink]="[]" (click)="toggleValidateEmail(false)">{{ "notYou" | i18n }}</a>
</div>
</ng-template>

View File

@ -6,12 +6,14 @@ import { first } from "rxjs/operators";
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component";
import { ApiService } from "@bitwarden/common/abstractions/api.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 { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service"; import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service"; import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.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<void>(); private destroy$ = new Subject<void>();
constructor( constructor(
apiService: ApiService,
appIdService: AppIdService,
authService: AuthService, authService: AuthService,
router: Router, router: Router,
i18nService: I18nService, i18nService: I18nService,
private route: ActivatedRoute, route: ActivatedRoute,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService, environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService, passwordGenerationService: PasswordGenerationService,
cryptoFunctionService: CryptoFunctionService, cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService,
private policyApiService: PolicyApiServiceAbstraction, private policyApiService: PolicyApiServiceAbstraction,
private policyService: InternalPolicyService, private policyService: InternalPolicyService,
logService: LogService, logService: LogService,
@ -56,9 +59,12 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
private messagingService: MessagingService, private messagingService: MessagingService,
private routerService: RouterService, private routerService: RouterService,
formBuilder: FormBuilder, formBuilder: FormBuilder,
formValidationErrorService: FormValidationErrorsService formValidationErrorService: FormValidationErrorsService,
loginService: LoginService
) { ) {
super( super(
apiService,
appIdService,
authService, authService,
router, router,
platformUtilsService, platformUtilsService,
@ -70,7 +76,9 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
logService, logService,
ngZone, ngZone,
formBuilder, formBuilder,
formValidationErrorService formValidationErrorService,
route,
loginService
); );
this.onSuccessfulLogin = async () => { this.onSuccessfulLogin = async () => {
this.messagingService.send("setFullWidth"); this.messagingService.send("setFullWidth");
@ -82,9 +90,6 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
async ngOnInit() { async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => { 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) { if (qParams.premium != null) {
this.routerService.setPreviousUrl("/settings/premium"); this.routerService.setPreviousUrl("/settings/premium");
} else if (qParams.org != null) { } else if (qParams.org != null) {
@ -102,8 +107,6 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
this.routerService.setPreviousUrl(route.toString()); this.routerService.setPreviousUrl(route.toString());
} }
await super.ngOnInit(); await super.ngOnInit();
const rememberEmail = await this.stateService.getRememberEmail();
this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
}); });
const invite = await this.stateService.getOrganizationInvitation(); const invite = await this.stateService.getOrganizationInvitation();
@ -176,6 +179,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
if (previousUrl) { if (previousUrl) {
this.router.navigateByUrl(previousUrl); this.router.navigateByUrl(previousUrl);
} else { } else {
this.loginService.clearValues();
this.router.navigate([this.successRoute]); this.router.navigate([this.successRoute]);
} }
} }

View File

@ -9,6 +9,7 @@ import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { TwoFactorService } from "@bitwarden/common/abstractions/twoFactor.service"; import { TwoFactorService } from "@bitwarden/common/abstractions/twoFactor.service";
@ -40,7 +41,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
logService: LogService, logService: LogService,
twoFactorService: TwoFactorService, twoFactorService: TwoFactorService,
appIdService: AppIdService, appIdService: AppIdService,
private routerService: RouterService private routerService: RouterService,
loginService: LoginService
) { ) {
super( super(
authService, authService,
@ -54,7 +56,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
route, route,
logService, logService,
twoFactorService, twoFactorService,
appIdService appIdService,
loginService
); );
this.onSuccessfulLoginNavigate = this.goAfterLogIn; this.onSuccessfulLoginNavigate = this.goAfterLogIn;
} }
@ -79,6 +82,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
} }
async goAfterLogIn() { async goAfterLogIn() {
this.loginService.clearValues();
const previousUrl = this.routerService.getPreviousUrl(); const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl) { if (previousUrl) {
this.router.navigateByUrl(previousUrl); this.router.navigateByUrl(previousUrl);

View File

@ -13,6 +13,7 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.
import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service";
import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/abstractions/i18n.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 { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.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 { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { StateFactory } from "@bitwarden/common/factories/stateFactory"; import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { LoginService } from "@bitwarden/common/services/login.service";
import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service"; import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service";
import { BroadcasterMessagingService } from "./broadcaster-messaging.service"; import { BroadcasterMessagingService } from "./broadcaster-messaging.service";
@ -98,6 +100,10 @@ import { WebPlatformUtilsService } from "./web-platform-utils.service";
provide: FileDownloadService, provide: FileDownloadService,
useClass: WebFileDownloadService, useClass: WebFileDownloadService,
}, },
{
provide: LoginServiceAbstraction,
useClass: LoginService,
},
], ],
}) })
export class CoreModule { export class CoreModule {

View File

@ -569,12 +569,15 @@
"loginOrCreateNewAccount": { "loginOrCreateNewAccount": {
"message": "Log in or create a new account to access your secure vault." "message": "Log in or create a new account to access your secure vault."
}, },
"loginWithDevice" : { "loginWithDevice": {
"message": "Log in with device" "message": "Log in with device"
}, },
"loginWithDeviceEnabledInfo": { "loginWithDeviceEnabledInfo": {
"message": "Log in with device must be set up in the settings of the Bitwarden mobile app. Need another option?" "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": { "createAccount": {
"message": "Create account" "message": "Create account"
}, },
@ -717,7 +720,7 @@
"noOrganizationsList": { "noOrganizationsList": {
"message": "You do not belong to any organizations. Organizations allow you to securely share items with other users." "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." "message": "A notification has been sent to your device."
}, },
"versionNumber": { "versionNumber": {
@ -5394,6 +5397,12 @@
"numberOfUsers": { "numberOfUsers": {
"message": "Number of users" "message": "Number of users"
}, },
"loggingInAs": {
"message": "Logging in as"
},
"notYou": {
"message": "Not you?"
},
"multiSelectPlaceholder": { "multiSelectPlaceholder": {
"message": "-- Type to Filter --" "message": "-- Type to Filter --"
}, },

View File

@ -1,12 +1,15 @@
import { Directive, OnInit } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PasswordHintRequest } from "@bitwarden/common/models/request/password-hint.request"; import { PasswordHintRequest } from "@bitwarden/common/models/request/password-hint.request";
export class HintComponent { @Directive()
export class HintComponent implements OnInit {
email = ""; email = "";
formPromise: Promise<any>; formPromise: Promise<any>;
@ -18,9 +21,14 @@ export class HintComponent {
protected i18nService: I18nService, protected i18nService: I18nService,
protected apiService: ApiService, protected apiService: ApiService,
protected platformUtilsService: PlatformUtilsService, protected platformUtilsService: PlatformUtilsService,
private logService: LogService private logService: LogService,
private loginService: LoginService
) {} ) {}
ngOnInit(): void {
this.email = this.loginService.getEmail() ?? "";
}
async submit() { async submit() {
if (this.email == null || this.email === "") { if (this.email == null || this.email === "") {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(

View File

@ -1,8 +1,10 @@
import { Directive, NgZone, OnInit } from "@angular/core"; import { Directive, NgZone, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; import { FormBuilder, Validators } from "@angular/forms";
import { Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { take } from "rxjs/operators"; 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 { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service"; import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
@ -12,6 +14,7 @@ import {
} from "@bitwarden/common/abstractions/formValidationErrors.service"; } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
@ -29,20 +32,30 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
onSuccessfulLoginNavigate: () => Promise<any>; onSuccessfulLoginNavigate: () => Promise<any>;
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>; onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
onSuccessfulLoginForceResetNavigate: () => Promise<any>; onSuccessfulLoginForceResetNavigate: () => Promise<any>;
selfHosted = false; private selfHosted = false;
showLoginWithDevice: boolean;
validatedEmail = false;
paramEmailSet = false;
formGroup = this.formBuilder.group({ formGroup = this.formBuilder.group({
email: ["", [Validators.required, Validators.email]], email: ["", [Validators.required, Validators.email]],
masterPassword: ["", [Validators.required, Validators.minLength(8)]], masterPassword: ["", [Validators.required, Validators.minLength(8)]],
rememberEmail: [true], rememberEmail: [false],
}); });
protected twoFactorRoute = "2fa"; protected twoFactorRoute = "2fa";
protected successRoute = "vault"; protected successRoute = "vault";
protected forcePasswordResetRoute = "update-temp-password"; protected forcePasswordResetRoute = "update-temp-password";
protected alwaysRememberEmail = false; protected alwaysRememberEmail = false;
protected skipRememberEmail = false;
get loggedEmail() {
return this.formGroup.value.email;
}
constructor( constructor(
protected apiService: ApiService,
protected appIdService: AppIdService,
protected authService: AuthService, protected authService: AuthService,
protected router: Router, protected router: Router,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
@ -54,7 +67,9 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
protected logService: LogService, protected logService: LogService,
protected ngZone: NgZone, protected ngZone: NgZone,
protected formBuilder: FormBuilder, protected formBuilder: FormBuilder,
protected formValidationErrorService: FormValidationErrorsService protected formValidationErrorService: FormValidationErrorsService,
protected route: ActivatedRoute,
protected loginService: LoginService
) { ) {
super(environmentService, i18nService, platformUtilsService); super(environmentService, i18nService, platformUtilsService);
this.selfHosted = platformUtilsService.isSelfHost(); this.selfHosted = platformUtilsService.isSelfHost();
@ -65,19 +80,35 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
} }
async ngOnInit() { 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 === "") { if (email == null || email === "") {
email = await this.stateService.getRememberedEmail(); email = await this.stateService.getRememberedEmail();
this.formGroup.get("email")?.setValue(email); }
if (email == null) { if (!this.paramEmailSet) {
this.formGroup.get("email")?.setValue(""); this.formGroup.get("email")?.setValue(email ?? "");
}
} }
if (!this.alwaysRememberEmail) { 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); this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
} }
if (email) {
this.validateEmail();
}
} }
async submit(showToast = true) { async submit(showToast = true) {
@ -108,6 +139,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
); );
this.formPromise = this.authService.logIn(credentials); this.formPromise = this.authService.logIn(credentials);
const response = await this.formPromise; const response = await this.formPromise;
this.setFormValues();
if (data.rememberEmail || this.alwaysRememberEmail) { if (data.rememberEmail || this.alwaysRememberEmail) {
await this.stateService.setRememberedEmail(data.email); await this.stateService.setRememberedEmail(data.email);
} else { } else {
@ -130,6 +162,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
} else { } else {
const disableFavicon = await this.stateService.getDisableFavicon(); const disableFavicon = await this.stateService.getDisableFavicon();
await this.stateService.setDisableFavicon(!!disableFavicon); await this.stateService.setDisableFavicon(!!disableFavicon);
this.loginService.clearValues();
if (this.onSuccessfulLogin != null) { if (this.onSuccessfulLogin != null) {
this.onSuccessfulLogin(); 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() { private getErrorToastMessage() {
const error: AllValidationErrors = this.formValidationErrorService const error: AllValidationErrors = this.formValidationErrorService
.getFormValidationErrors(this.formGroup.controls) .getFormValidationErrors(this.formGroup.controls)
@ -213,8 +265,19 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
return `${error.controlName}${name}`; 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() { protected focusInput() {
const email = this.formGroup.value.email; const email = this.loggedEmail;
document.getElementById(email == null || email === "" ? "email" : "masterPassword").focus(); document.getElementById(email == null || email === "" ? "email" : "masterPassword").focus();
} }
} }

View File

@ -9,6 +9,7 @@ import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service"; import { StateService } from "@bitwarden/common/abstractions/state.service";
import { TwoFactorService } from "@bitwarden/common/abstractions/twoFactor.service"; import { TwoFactorService } from "@bitwarden/common/abstractions/twoFactor.service";
@ -59,7 +60,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected logService: LogService, protected logService: LogService,
protected twoFactorService: TwoFactorService, protected twoFactorService: TwoFactorService,
protected appIdService: AppIdService protected appIdService: AppIdService,
protected loginService: LoginService
) { ) {
super(environmentService, i18nService, platformUtilsService); super(environmentService, i18nService, platformUtilsService);
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
@ -204,6 +206,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
return; return;
} }
if (this.onSuccessfulLogin != null) { if (this.onSuccessfulLogin != null) {
this.loginService.clearValues();
this.onSuccessfulLogin(); this.onSuccessfulLogin();
} }
if (response.resetMasterPassword) { if (response.resetMasterPassword) {
@ -213,8 +216,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
this.successRoute = "update-temp-password"; this.successRoute = "update-temp-password";
} }
if (this.onSuccessfulLoginNavigate != null) { if (this.onSuccessfulLoginNavigate != null) {
this.loginService.clearValues();
this.onSuccessfulLoginNavigate(); this.onSuccessfulLoginNavigate();
} else { } else {
this.loginService.clearValues();
this.router.navigate([this.successRoute], { this.router.navigate([this.successRoute], {
queryParams: { queryParams: {
identifier: this.identifier, identifier: this.identifier,

View File

@ -31,6 +31,7 @@ import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction }
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/abstractions/i18n.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/abstractions/i18n.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/abstractions/keyConnector.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/abstractions/keyConnector.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service";
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; 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 { FolderService } from "@bitwarden/common/services/folder/folder.service";
import { FormValidationErrorsService } from "@bitwarden/common/services/formValidationErrors.service"; import { FormValidationErrorsService } from "@bitwarden/common/services/formValidationErrors.service";
import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.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 { NotificationsService } from "@bitwarden/common/services/notifications.service";
import { OrganizationApiService } from "@bitwarden/common/services/organization/organization-api.service"; import { OrganizationApiService } from "@bitwarden/common/services/organization/organization-api.service";
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service"; import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
@ -578,6 +580,10 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
useClass: ValidationService, useClass: ValidationService,
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
}, },
{
provide: LoginServiceAbstraction,
useClass: LoginService,
},
], ],
}) })
export class JslibServicesModule {} export class JslibServicesModule {}

View File

@ -479,6 +479,7 @@ export abstract class ApiService {
putDeviceVerificationSettings: ( putDeviceVerificationSettings: (
request: DeviceVerificationRequest request: DeviceVerificationRequest
) => Promise<DeviceVerificationResponse>; ) => Promise<DeviceVerificationResponse>;
getKnownDevice: (email: string, deviceIdentifier: string) => Promise<boolean>;
getEmergencyAccessTrusted: () => Promise<ListResponse<EmergencyAccessGranteeDetailsResponse>>; getEmergencyAccessTrusted: () => Promise<ListResponse<EmergencyAccessGranteeDetailsResponse>>;
getEmergencyAccessGranted: () => Promise<ListResponse<EmergencyAccessGrantorDetailsResponse>>; getEmergencyAccessGranted: () => Promise<ListResponse<EmergencyAccessGrantorDetailsResponse>>;

View File

@ -0,0 +1,7 @@
export abstract class LoginService {
getEmail: () => string;
getRememberEmail: () => boolean;
setEmail: (value: string) => void;
setRememberEmail: (value: boolean) => void;
clearValues: () => void;
}

View File

@ -1518,6 +1518,12 @@ export class ApiService implements ApiServiceAbstraction {
return new DeviceVerificationResponse(r); return new DeviceVerificationResponse(r);
} }
async getKnownDevice(email: string, deviceIdentifier: string): Promise<boolean> {
const path = `/devices/knowndevice/${email}/${deviceIdentifier}`;
const r = await this.send("GET", path, null, false, true);
return r as boolean;
}
// Emergency Access APIs // Emergency Access APIs
async getEmergencyAccessTrusted(): Promise<ListResponse<EmergencyAccessGranteeDetailsResponse>> { async getEmergencyAccessTrusted(): Promise<ListResponse<EmergencyAccessGranteeDetailsResponse>> {

View File

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