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"
}
}
},
"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">
<header>
<div class="left">
<button type="button" routerLink="/login">{{ "cancel" | i18n }}</button>
<button type="button" routerLink="/login">
{{ "cancel" | i18n }}
</button>
</div>
<h1 class="center">
<span class="title">{{ "passwordHint" | i18n }}</span>

View File

@ -1,10 +1,11 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { ActivatedRoute, Router } from "@angular/router";
import { HintComponent as BaseHintComponent } from "@bitwarden/angular/components/hint.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { LoginService } from "@bitwarden/common/abstractions/login.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@Component({
@ -17,8 +18,14 @@ export class HintComponent extends BaseHintComponent {
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
apiService: ApiService,
logService: LogService
logService: LogService,
private route: ActivatedRoute,
loginService: LoginService
) {
super(router, i18nService, apiService, platformUtilsService, logService);
super(router, i18nService, apiService, platformUtilsService, logService, loginService);
super.onSuccessfulSubmit = async () => {
this.router.navigate([this.successRoute]);
};
}
}

View File

@ -2,15 +2,28 @@
<div class="content">
<div class="logo-image"></div>
<p class="lead text-center">{{ "loginOrCreateNewAccount" | i18n }}</p>
<button type="button" class="btn primary block" routerLink="/login">
<b>{{ "login" | i18n }}</b>
</button>
<button type="button" (click)="launchSsoBrowser()" class="btn block">
<i class="bwi bwi-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
</button>
<button type="button" class="btn block" routerLink="/register">
{{ "createAccount" | i18n }}
</button>
<form #form [formGroup]="formGroup" (ngSubmit)="submit()">
<div class="box">
<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>
<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>
<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 { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { Utils } from "@bitwarden/common/misc/utils";
@Component({
selector: "app-home",
templateUrl: "home.component.html",
})
export class HomeComponent {
export class HomeComponent implements OnInit {
loginInitiated = false;
formGroup = this.formBuilder.group({
email: ["", [Validators.required, Validators.email]],
});
constructor(
protected platformUtilsService: PlatformUtilsService,
private passwordGenerationService: PasswordGenerationService,
private stateService: StateService,
private cryptoFunctionService: CryptoFunctionService,
private environmentService: EnvironmentService
private formBuilder: FormBuilder,
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() {
// 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";
submit() {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccured"),
this.i18nService.t("invalidEmail")
);
return;
}
const redirectUri = url + "/sso-connector.html";
this.stateService.setRememberedEmail(this.formGroup.value.email);
// Launch browser
this.platformUtilsService.launchUri(
url +
"/#/sso?clientId=browser" +
"&redirectUri=" +
encodeURIComponent(redirectUri) +
"&state=" +
state +
"&codeChallenge=" +
codeChallenge
);
this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } });
}
get selfHostedDomain() {
return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null;
}
}

View File

@ -1,25 +1,12 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" [formGroup]="formGroup">
<header>
<div class="left">
<button type="button" routerLink="/home">{{ "cancel" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ "appName" | i18n }}</span>
<h1 class="login-center">
<span class="title">{{ "logIn" | i18n }}</span>
</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>
<main tabindex="-1">
<div class="box">
<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>
@ -52,13 +39,27 @@
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
</div>
<div class="box-footer">
<button type="button" class="btn link" routerLink="/hint" (click)="setFormValues()">
<b>{{ "getMasterPasswordHint" | i18n }}</b>
</button>
</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>
<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>
</form>

View File

@ -1,27 +1,33 @@
import { Component, NgZone } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Router } from "@angular/router";
import { ActivatedRoute, Router } from "@angular/router";
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { LoginService } from "@bitwarden/common/abstractions/login.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { Utils } from "@bitwarden/common/misc/utils";
@Component({
selector: "app-login",
templateUrl: "login.component.html",
})
export class LoginComponent extends BaseLoginComponent {
protected alwaysRememberEmail = true;
protected skipRememberEmail = true;
constructor(
apiService: ApiService,
appIdService: AppIdService,
authService: AuthService,
router: Router,
protected platformUtilsService: PlatformUtilsService,
@ -34,9 +40,13 @@ export class LoginComponent extends BaseLoginComponent {
logService: LogService,
ngZone: NgZone,
formBuilder: FormBuilder,
formValidationErrorService: FormValidationErrorsService
formValidationErrorService: FormValidationErrorsService,
route: ActivatedRoute,
loginService: LoginService
) {
super(
apiService,
appIdService,
authService,
router,
platformUtilsService,
@ -48,7 +58,9 @@ export class LoginComponent extends BaseLoginComponent {
logService,
ngZone,
formBuilder,
formValidationErrorService
formValidationErrorService,
route,
loginService
);
super.onSuccessfulLogin = async () => {
await syncService.fullSync(true);
@ -59,4 +71,45 @@ export class LoginComponent extends BaseLoginComponent {
settings() {
this.router.navigate(["environment"]);
}
async launchSsoBrowser() {
// Generate necessary sso params
const passwordOptions: any = {
type: "password",
length: 64,
uppercase: true,
lowercase: true,
numbers: true,
special: false,
};
const state =
(await this.passwordGenerationService.generatePassword(passwordOptions)) +
":clientId=browser";
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
await this.stateService.setSsoCodeVerifier(codeVerifier);
await this.stateService.setSsoState(state);
let url = this.environmentService.getWebVaultUrl();
if (url == null) {
url = "https://vault.bitwarden.com";
}
const redirectUri = url + "/sso-connector.html";
// Launch browser
this.platformUtilsService.launchUri(
url +
"/#/sso?clientId=browser" +
"&redirectUri=" +
encodeURIComponent(redirectUri) +
"&state=" +
state +
"&codeChallenge=" +
codeChallenge
);
}
}

View File

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

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 }}
<a href="https://bitwarden.com/help/article/private-mode/" target="_blank" rel="noopener">{{
"learnMore" | i18n

View File

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

View File

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

View File

@ -440,3 +440,7 @@ app-vault-view .box-footer {
html.force_redraw {
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;
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 { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service";
import { LogService as LogServiceAbstraction } from "@bitwarden/common/abstractions/log.service";
import { LoginService as LoginServiceAbstraction } from "@bitwarden/common/abstractions/login.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
@ -48,6 +49,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { AuthService } from "@bitwarden/common/services/auth.service";
import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service";
import { LoginService } from "@bitwarden/common/services/login.service";
import { SearchService } from "@bitwarden/common/services/search.service";
import MainBackground from "../../background/main.background";
@ -309,6 +311,10 @@ function getBgService<T>(service: keyof MainBackground) {
provide: FileDownloadService,
useClass: BrowserFileDownloadService,
},
{
provide: LoginServiceAbstraction,
useClass: LoginService,
},
{
provide: AbstractThemingService,
useFactory: () => {

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;

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 {
i18nService: I18nService,
apiService: ApiService,
platformUtilsService: PlatformUtilsService,
logService: LogService
logService: LogService,
loginService: LoginService
) {
super(router, i18nService, apiService, platformUtilsService, logService);
super(router, i18nService, apiService, platformUtilsService, logService, loginService);
}
}

View File

@ -16,102 +16,122 @@
<div
class="tw-mt-3 tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<bit-callout
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
*ngIf="showResetPasswordAutoEnrollWarning"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</bit-callout>
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
<input id="login_input_email" bitInput type="email" formControlName="email" />
</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"
/>
<ng-container *ngIf="!validatedEmail; else loginPage">
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "emailAddress" | i18n }}</bit-label>
<input
id="login_input_email"
bitInput
type="email"
formControlName="email"
(keyup.enter)="validateEmail()"
/>
</bit-form-field>
</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()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
<div class="tw-mb-3">
<button
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">
<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>
<hr />
<a bitButton buttonType="secondary" routerLink="/register" [block]="true">
<i class="bwi bwi-pencil-square"></i>
{{ "createAccount" | i18n }}
</a>
</div>
<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>
<p class="tw-m-0 tw-text-sm">
{{ "newAroundHere" | i18n }}
<a routerLink="/register">{{ "createAccount" | i18n }}</a>
</p>
</ng-container>
</div>
</div>
</div>
</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 { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { LoginService } from "@bitwarden/common/abstractions/login.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@ -39,15 +41,16 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
private destroy$ = new Subject<void>();
constructor(
apiService: ApiService,
appIdService: AppIdService,
authService: AuthService,
router: Router,
i18nService: I18nService,
private route: ActivatedRoute,
route: ActivatedRoute,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService,
cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService,
private policyApiService: PolicyApiServiceAbstraction,
private policyService: InternalPolicyService,
logService: LogService,
@ -56,9 +59,12 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
private messagingService: MessagingService,
private routerService: RouterService,
formBuilder: FormBuilder,
formValidationErrorService: FormValidationErrorsService
formValidationErrorService: FormValidationErrorsService,
loginService: LoginService
) {
super(
apiService,
appIdService,
authService,
router,
platformUtilsService,
@ -70,7 +76,9 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
logService,
ngZone,
formBuilder,
formValidationErrorService
formValidationErrorService,
route,
loginService
);
this.onSuccessfulLogin = async () => {
this.messagingService.send("setFullWidth");
@ -82,9 +90,6 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
this.formGroup.get("email")?.setValue(qParams.email);
}
if (qParams.premium != null) {
this.routerService.setPreviousUrl("/settings/premium");
} else if (qParams.org != null) {
@ -102,8 +107,6 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
this.routerService.setPreviousUrl(route.toString());
}
await super.ngOnInit();
const rememberEmail = await this.stateService.getRememberEmail();
this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
});
const invite = await this.stateService.getOrganizationInvitation();
@ -176,6 +179,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
if (previousUrl) {
this.router.navigateByUrl(previousUrl);
} else {
this.loginService.clearValues();
this.router.navigate([this.successRoute]);
}
}

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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