1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-27 05:33:59 +00:00

Two-Step Login (#3852)

* [SG-163] Two step login flow web (#3648)

* two step login flow

* moved code from old branch and reafctored

* fixed review comments

* [SG-164] Two Step Login Flow - Browser (#3793)

* Add new messages

* Remove SSO button from home component

* Change create account button to text

* Add top padding to create account link

* Add email input to HomeComponent

* Add continue button to email input

* Add form to home component

* Retreive email from state service

* Redirect to login after submit

* Add error message for invalid email

* Remove email input from login component

* Remove loggingInTo from under MP input

* Style the MP hint link

* Add self hosted domain to email form

* Made the mp hint link bold

* Add the new login button

* Style app-private-mode-warning in its component

* Bitwarden -> Login text change

* Remove the old login button

* Cancel -> Close text change

* Add avatar to login header

* Login -> LoginWithMasterPassword text change

* Add SSO button to login screen

* Add not you button

* Allow all clients to use the email query param on the login component

* Introduct HomeGuard

* Clear remembered email when clicking Not You

* Make remember email opt-in

* Use formGroup.patchValue instead of directly patching individual controls

* [SG-165] Desktop login flow changes (#3814)

* two step login flow

* moved code from old branch and reafctored

* fixed review comments

* Make toggleValidateEmail in base class public

* Add desktop login messages

* Desktop login flow changes

* Fix known device api error

* Only submit if email has been validated

* Clear remembered email when switching accounts

* Fix merge issue

* Add 'login with another device' button

* Remove 'log in with another device' button for now

* Pin login pag content to top instead of center justified

* Leave email if 'Not you?' is clicked

* Continue when enter is hit on email input

Co-authored-by: gbubemismith <gsmithwalter@gmail.com>

* [SG-750] and [SG-751] Web two step login bug fixes (#3843)

* Continue when enter is hit on email input

* Mark email input as touched on 'continue' so field is validated

* disable login with device on self-hosted (#3895)

* [SG-753] Keep email after hint component is launched in browser (#3883)

* Keep email after hint component is launched in browser

* Use query params instead of state for consistency

* Send email and rememberEmail to home component on navigation (#3897)

* removed avatar and close button from the password screen (#3901)

* [SG-781] Remove extra login page and remove rememberEmail code (#3902)

* Remove browser home guard

* Always remember email for browser

* Remove login landing page button

* [SG-782] Add login service to streamline login form data persistence (#3911)

* Add login service and abstraction

* Inject login service into apps

* Inject and use new service in login component

* Use service in hint component to prefill email

* Add method in LoginService to clear service values

* Add LoginService to two-factor component to clear values

* make login.service variables private

Co-authored-by: Gbubemi Smith <gsmith@bitwarden.com>
Co-authored-by: Addison Beck <addisonbeck1@gmail.com>
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
Co-authored-by: gbubemismith <gsmithwalter@gmail.com>
This commit is contained in:
Todd Martin
2022-10-28 14:54:55 -04:00
committed by GitHub
parent aa256b8a70
commit 2cd65939d5
38 changed files with 703 additions and 269 deletions

View File

@@ -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: () => {