From 2357957c2f0ada591046e6df76e1c78e1561eeed Mon Sep 17 00:00:00 2001 From: Gbubemi Smith Date: Mon, 26 Sep 2022 23:26:10 +0100 Subject: [PATCH 01/20] [SG-168] Passwordless login web MVP (#3424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * passwordless login page redesign * passwordless login page redesign * restyled login form to use tailwind * restyled login form to use tailwind * moved texts on login device template to locales * made reactive form changes for clients * added request model * made more changes * added implmentation to auth request api * fixed refrencing issue * renamed model property * Added resend notification functionality * Added new file * login with device first draft * login with device first draft * login with device first draft * login with device first draft * connection to anonymous hub * connection to anonymous hub * refactored confirm login response * removed comment * cleaned up login * changed uptyped form builder * changed uptyped form builder * [SG-168] Update login strategy with passwordless login credentials. * [SG-168] Removed logs. Changed inputs for passwordless logic strategy. Removed tokenRequestPasswordless it is using the same as password. * code cleanup * code cleanup * removed login with device from self hosted * fixed PR comments * added module for login * fixed post request bug * added feature flag * added feature flag * added feature flag Co-authored-by: André Bispo (cherry picked from commit 22a878792e5fddd9bd0396edec82c8cfa47e2390) --- .../src/popup/accounts/login.component.html | 16 +- .../src/popup/accounts/login.component.ts | 10 +- .../src/app/accounts/login.component.html | 14 +- .../src/app/accounts/login.component.ts | 10 +- apps/web/config/base.json | 4 +- apps/web/config/cloud.json | 3 +- apps/web/config/development.json | 3 +- apps/web/config/qa.json | 3 +- apps/web/config/selfhosted.json | 3 +- .../web/src/app/accounts/login.component.html | 102 ---------- .../login/login-with-device.component.html | 44 +++++ .../login/login-with-device.component.ts | 175 ++++++++++++++++++ .../app/accounts/login/login.component.html | 121 ++++++++++++ .../accounts/{ => login}/login.component.ts | 52 ++++-- .../src/app/accounts/login/login.module.ts | 13 ++ apps/web/src/app/oss-routing.module.ts | 8 +- apps/web/src/app/oss.module.ts | 3 + .../src/app/shared/loose-components.module.ts | 3 - apps/web/src/locales/en/messages.json | 29 ++- apps/web/src/utils/flags.ts | 1 + .../angular/src/components/login.component.ts | 110 ++++++----- .../src/services/jslib-services.module.ts | 7 + .../src/abstractions/anonymousHub.service.ts | 4 + libs/common/src/abstractions/api.service.ts | 5 + libs/common/src/abstractions/auth.service.ts | 13 +- libs/common/src/enums/authRequestType.ts | 4 + libs/common/src/enums/authenticationType.ts | 1 + libs/common/src/enums/notificationType.ts | 3 + .../misc/logInStrategies/logIn.strategy.ts | 7 +- .../passwordlessLogin.strategy.ts | 86 +++++++++ .../src/models/domain/logInCredentials.ts | 15 ++ .../request/identityToken/tokenRequest.ts | 10 + .../request/passwordlessCreateAuthRequest.ts | 12 ++ .../models/response/authRequestResponse.ts | 26 +++ .../models/response/notificationResponse.ts | 15 ++ .../src/services/anonymousHub.service.ts | 60 ++++++ libs/common/src/services/api.service.ts | 13 ++ libs/common/src/services/auth.service.ts | 141 +++++++++----- 38 files changed, 908 insertions(+), 241 deletions(-) delete mode 100644 apps/web/src/app/accounts/login.component.html create mode 100644 apps/web/src/app/accounts/login/login-with-device.component.html create mode 100644 apps/web/src/app/accounts/login/login-with-device.component.ts create mode 100644 apps/web/src/app/accounts/login/login.component.html rename apps/web/src/app/accounts/{ => login}/login.component.ts (80%) create mode 100644 apps/web/src/app/accounts/login/login.module.ts create mode 100644 libs/common/src/abstractions/anonymousHub.service.ts create mode 100644 libs/common/src/enums/authRequestType.ts create mode 100644 libs/common/src/misc/logInStrategies/passwordlessLogin.strategy.ts create mode 100644 libs/common/src/models/request/passwordlessCreateAuthRequest.ts create mode 100644 libs/common/src/models/response/authRequestResponse.ts create mode 100644 libs/common/src/services/anonymousHub.service.ts diff --git a/apps/browser/src/popup/accounts/login.component.html b/apps/browser/src/popup/accounts/login.component.html index 4c18805f452..1b32f63819a 100644 --- a/apps/browser/src/popup/accounts/login.component.html +++ b/apps/browser/src/popup/accounts/login.component.html @@ -1,4 +1,4 @@ -
+
@@ -18,15 +18,7 @@
- +
@@ -34,10 +26,8 @@
diff --git a/apps/browser/src/popup/accounts/login.component.ts b/apps/browser/src/popup/accounts/login.component.ts index 83c654d6737..5028af06bfb 100644 --- a/apps/browser/src/popup/accounts/login.component.ts +++ b/apps/browser/src/popup/accounts/login.component.ts @@ -1,10 +1,12 @@ import { Component, NgZone } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { Router } from "@angular/router"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component"; 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 { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; @@ -30,7 +32,9 @@ export class LoginComponent extends BaseLoginComponent { protected cryptoFunctionService: CryptoFunctionService, syncService: SyncService, logService: LogService, - ngZone: NgZone + ngZone: NgZone, + formBuilder: FormBuilder, + formValidationErrorService: FormValidationErrorsService ) { super( authService, @@ -42,7 +46,9 @@ export class LoginComponent extends BaseLoginComponent { passwordGenerationService, cryptoFunctionService, logService, - ngZone + ngZone, + formBuilder, + formValidationErrorService ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); diff --git a/apps/desktop/src/app/accounts/login.component.html b/apps/desktop/src/app/accounts/login.component.html index 314b0550621..c11ed881b00 100644 --- a/apps/desktop/src/app/accounts/login.component.html +++ b/apps/desktop/src/app/accounts/login.component.html @@ -16,6 +16,7 @@ #form (ngSubmit)="submit()" [appApiAction]="formPromise" + [formGroup]="formGroup" attr.aria-hidden="{{ showingModal }}" >
@@ -25,14 +26,7 @@
- +
@@ -40,10 +34,8 @@
diff --git a/apps/desktop/src/app/accounts/login.component.ts b/apps/desktop/src/app/accounts/login.component.ts index 959c8a45650..33eefbd57ec 100644 --- a/apps/desktop/src/app/accounts/login.component.ts +++ b/apps/desktop/src/app/accounts/login.component.ts @@ -1,4 +1,5 @@ import { Component, NgZone, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { Router } from "@angular/router"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/components/login.component"; @@ -7,6 +8,7 @@ import { AuthService } from "@bitwarden/common/abstractions/auth.service"; import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.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 { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; @@ -47,7 +49,9 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { private broadcasterService: BroadcasterService, ngZone: NgZone, private messagingService: MessagingService, - logService: LogService + logService: LogService, + formBuilder: FormBuilder, + formValidationErrorService: FormValidationErrorsService ) { super( authService, @@ -59,7 +63,9 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { passwordGenerationService, cryptoFunctionService, logService, - ngZone + ngZone, + formBuilder, + formValidationErrorService ); super.onSuccessfulLogin = () => { return syncService.fullSync(true); diff --git a/apps/web/config/base.json b/apps/web/config/base.json index cab6fbe950f..8eb8a311335 100644 --- a/apps/web/config/base.json +++ b/apps/web/config/base.json @@ -10,5 +10,7 @@ "port": 8080, "allowedHosts": "auto" }, - "flags": {} + "flags": { + "showPasswordless": false + } } diff --git a/apps/web/config/cloud.json b/apps/web/config/cloud.json index 96d692f7e8d..5bd5e6b0608 100644 --- a/apps/web/config/cloud.json +++ b/apps/web/config/cloud.json @@ -16,6 +16,7 @@ "proxyEvents": "https://events.bitwarden.com" }, "flags": { - "showTrial": true + "showTrial": true, + "showPasswordless": false } } diff --git a/apps/web/config/development.json b/apps/web/config/development.json index e3048db7a22..f460a1659aa 100644 --- a/apps/web/config/development.json +++ b/apps/web/config/development.json @@ -10,6 +10,7 @@ "proxyNotifications": "http://localhost:61840" }, "flags": { - "showTrial": true + "showTrial": true, + "showPasswordless": true } } diff --git a/apps/web/config/qa.json b/apps/web/config/qa.json index 4371ea1ff98..a0d1b0e88c3 100644 --- a/apps/web/config/qa.json +++ b/apps/web/config/qa.json @@ -10,6 +10,7 @@ "proxyEvents": "https://events.qa.bitwarden.pw" }, "flags": { - "showTrial": true + "showTrial": true, + "showPasswordless": true } } diff --git a/apps/web/config/selfhosted.json b/apps/web/config/selfhosted.json index 3ba61fda596..b37a9226043 100644 --- a/apps/web/config/selfhosted.json +++ b/apps/web/config/selfhosted.json @@ -7,6 +7,7 @@ "port": 8081 }, "flags": { - "showTrial": false + "showTrial": false, + "showPasswordless": false } } diff --git a/apps/web/src/app/accounts/login.component.html b/apps/web/src/app/accounts/login.component.html deleted file mode 100644 index e0c4ef68db7..00000000000 --- a/apps/web/src/app/accounts/login.component.html +++ /dev/null @@ -1,102 +0,0 @@ - -
-
- -

{{ "loginOrCreateNewAccount" | i18n }}

-
-
- - {{ "resetPasswordAutoEnrollInviteWarning" | i18n }} - -
- - -
-
- -
- - -
- - {{ "getMasterPasswordHint" | i18n }} - -
-
- - -
-
- -
-
-
- - - - {{ "createAccount" | i18n }} - -
- -
-
-
-
- diff --git a/apps/web/src/app/accounts/login/login-with-device.component.html b/apps/web/src/app/accounts/login/login-with-device.component.html new file mode 100644 index 00000000000..3105a639ad3 --- /dev/null +++ b/apps/web/src/app/accounts/login/login-with-device.component.html @@ -0,0 +1,44 @@ +
+
+ +

+ {{ "loginOrCreateNewAccount" | i18n }} +

+ +
+

{{ "logInInitiated" | i18n }}

+ +
+

{{ "notificationSentDevice" | i18n }}

+ +

+ {{ "fingerprintMatchInfo" | i18n }} +

+
+ +
+

{{ "fingerprintPhraseHeader" | i18n }}

+

+ {{ passwordlessRequest?.fingerprintPhrase }} +

+
+ + + +
+ +
+ {{ "loginWithDevciceEnabledInfo" | i18n }} + {{ "viewAllLoginOptions" | i18n }} +
+
+
+
diff --git a/apps/web/src/app/accounts/login/login-with-device.component.ts b/apps/web/src/app/accounts/login/login-with-device.component.ts new file mode 100644 index 00000000000..4c6f6268dfd --- /dev/null +++ b/apps/web/src/app/accounts/login/login-with-device.component.ts @@ -0,0 +1,175 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; + +import { CaptchaProtectedComponent } from "@bitwarden/angular/components/captchaProtected.component"; +import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.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 { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; +import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.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 { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; +import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; +import { StateService } from "@bitwarden/common/abstractions/state.service"; +import { AuthRequestType } from "@bitwarden/common/enums/authRequestType"; +import { Utils } from "@bitwarden/common/misc/utils"; +import { PasswordlessLogInCredentials } from "@bitwarden/common/models/domain/logInCredentials"; +import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; +import { PasswordlessCreateAuthRequest } from "@bitwarden/common/models/request/passwordlessCreateAuthRequest"; +import { AuthRequestResponse } from "@bitwarden/common/models/response/authRequestResponse"; + +@Component({ + selector: "app-login-with-device", + templateUrl: "login-with-device.component.html", +}) +export class LoginWithDeviceComponent + extends CaptchaProtectedComponent + implements OnInit, OnDestroy +{ + private destroy$ = new Subject(); + email: string; + showResendNotification = false; + passwordlessRequest: PasswordlessCreateAuthRequest; + onSuccessfulLoginTwoFactorNavigate: () => Promise; + onSuccessfulLogin: () => Promise; + onSuccessfulLoginNavigate: () => Promise; + + protected twoFactorRoute = "2fa"; + protected successRoute = "vault"; + private authRequestKeyPair: [publicKey: ArrayBuffer, privateKey: ArrayBuffer]; + + constructor( + private router: Router, + private cryptoService: CryptoService, + private cryptoFunctionService: CryptoFunctionService, + private appIdService: AppIdService, + private passwordGenerationService: PasswordGenerationService, + private apiService: ApiService, + private authService: AuthService, + private logService: LogService, + private stateService: StateService, + environmentService: EnvironmentService, + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + private anonymousHubService: AnonymousHubService + ) { + super(environmentService, i18nService, platformUtilsService); + + const navigation = this.router.getCurrentNavigation(); + if (navigation) { + this.email = navigation.extras?.state?.email; + } + + //gets signalR push notification + this.authService + .getPushNotifcationObs$() + .pipe(takeUntil(this.destroy$)) + .subscribe((id) => { + this.confirmResponse(id); + }); + } + + async ngOnInit() { + if (!this.email) { + this.router.navigate(["/login"]); + return; + } + + this.startPasswordlessLogin(); + } + + async startPasswordlessLogin() { + this.showResendNotification = false; + + try { + await this.buildAuthRequest(); + const reqResponse = await this.apiService.postAuthRequest(this.passwordlessRequest); + + if (reqResponse.id) { + this.anonymousHubService.createHubConnection(reqResponse.id); + } + } catch (e) { + this.logService.error(e); + } + + setTimeout(() => { + this.showResendNotification = true; + }, 12000); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + this.anonymousHubService.stopHubConnection(); + } + + private async confirmResponse(requestId: string) { + try { + const response = await this.apiService.getAuthResponse( + requestId, + this.passwordlessRequest.accessCode + ); + + if (!response.requestApproved) { + return; + } + + const credentials = await this.buildLoginCredntials(requestId, response); + await this.authService.logIn(credentials); + if (this.onSuccessfulLogin != null) { + this.onSuccessfulLogin(); + } + if (this.onSuccessfulLoginNavigate != null) { + this.onSuccessfulLoginNavigate(); + } else { + this.router.navigate([this.successRoute]); + } + } catch (error) { + this.logService.error(error); + } + } + + private async buildAuthRequest() { + this.authRequestKeyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); + const fingerprint = await ( + await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair[0]) + ).join("-"); + const deviceIdentifier = await this.appIdService.getAppId(); + const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair[0]); + const accessCode = await this.passwordGenerationService.generatePassword({ length: 25 }); + + this.passwordlessRequest = new PasswordlessCreateAuthRequest( + this.email, + deviceIdentifier, + publicKey, + AuthRequestType.AuthenticateAndUnlock, + accessCode, + fingerprint + ); + } + + private async buildLoginCredntials( + requestId: string, + response: AuthRequestResponse + ): Promise { + const decKey = await this.cryptoService.rsaDecrypt(response.key, this.authRequestKeyPair[1]); + const decMasterPasswordHash = await this.cryptoService.rsaDecrypt( + response.masterPasswordHash, + this.authRequestKeyPair[1] + ); + const key = new SymmetricCryptoKey(decKey); + const localHashedPassword = Utils.fromBufferToUtf8(decMasterPasswordHash); + + return new PasswordlessLogInCredentials( + this.email, + this.passwordlessRequest.accessCode, + requestId, + key, + localHashedPassword + ); + } +} diff --git a/apps/web/src/app/accounts/login/login.component.html b/apps/web/src/app/accounts/login/login.component.html new file mode 100644 index 00000000000..7df9777f39c --- /dev/null +++ b/apps/web/src/app/accounts/login/login.component.html @@ -0,0 +1,121 @@ +
+
+
+ +

+ {{ "loginOrCreateNewAccount" | i18n }} +

+
+ + {{ "resetPasswordAutoEnrollInviteWarning" | i18n }} + + +
+ + {{ "emailAddress" | i18n }} + + +
+ +
+ + {{ "masterPass" | i18n }} + + + + {{ "getMasterPasswordHint" | i18n }} + + +
+ +
+
+ +
+ + {{ "rememberEmail" | i18n }} + +
+ +
+ +
+ +
+ +
+ + + + + {{ "createAccount" | i18n }} + +
+ +
+ +
+ + +
+
+
+
diff --git a/apps/web/src/app/accounts/login.component.ts b/apps/web/src/app/accounts/login/login.component.ts similarity index 80% rename from apps/web/src/app/accounts/login.component.ts rename to apps/web/src/app/accounts/login/login.component.ts index 6a13ff32337..8664ae4a572 100644 --- a/apps/web/src/app/accounts/login.component.ts +++ b/apps/web/src/app/accounts/login/login.component.ts @@ -1,4 +1,5 @@ import { Component, NgZone } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; @@ -7,6 +8,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.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 { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; @@ -20,7 +22,9 @@ import { Policy } from "@bitwarden/common/models/domain/policy"; import { ListResponse } from "@bitwarden/common/models/response/listResponse"; import { PolicyResponse } from "@bitwarden/common/models/response/policyResponse"; -import { RouterService, StateService } from "../core"; +import { flagEnabled } from "src/utils/flags"; + +import { RouterService, StateService } from "../../core"; @Component({ selector: "app-login", @@ -31,6 +35,7 @@ export class LoginComponent extends BaseLoginComponent { showResetPasswordAutoEnrollWarning = false; enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions; policies: ListResponse; + showPasswordless = false; constructor( authService: AuthService, @@ -48,7 +53,9 @@ export class LoginComponent extends BaseLoginComponent { ngZone: NgZone, protected stateService: StateService, private messagingService: MessagingService, - private routerService: RouterService + private routerService: RouterService, + formBuilder: FormBuilder, + formValidationErrorService: FormValidationErrorsService ) { super( authService, @@ -60,19 +67,22 @@ export class LoginComponent extends BaseLoginComponent { passwordGenerationService, cryptoFunctionService, logService, - ngZone + ngZone, + formBuilder, + formValidationErrorService ); this.onSuccessfulLogin = async () => { this.messagingService.send("setFullWidth"); }; this.onSuccessfulLoginNavigate = this.goAfterLogIn; + this.showPasswordless = flagEnabled("showPasswordless"); } 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.email = qParams.email; + this.formGroup.get("email")?.setValue(qParams.email); } if (qParams.premium != null) { this.routerService.setPreviousUrl("/settings/premium"); @@ -91,7 +101,8 @@ export class LoginComponent extends BaseLoginComponent { this.routerService.setPreviousUrl(route.toString()); } await super.ngOnInit(); - this.rememberEmail = await this.stateService.getRememberEmail(); + const rememberEmail = await this.stateService.getRememberEmail(); + this.formGroup.get("rememberEmail")?.setValue(rememberEmail); }); const invite = await this.stateService.getOrganizationInvitation(); @@ -125,10 +136,12 @@ export class LoginComponent extends BaseLoginComponent { } async goAfterLogIn() { + const masterPassword = this.formGroup.get("masterPassword")?.value; + // Check master password against policy if (this.enforcedPasswordPolicyOptions != null) { const strengthResult = this.passwordGenerationService.passwordStrength( - this.masterPassword, + masterPassword, this.getPasswordStrengthUserInput() ); const masterPasswordScore = strengthResult == null ? null : strengthResult.score; @@ -137,7 +150,7 @@ export class LoginComponent extends BaseLoginComponent { if ( !this.policyService.evaluateMasterPassword( masterPasswordScore, - this.masterPassword, + masterPassword, this.enforcedPasswordPolicyOptions ) ) { @@ -158,19 +171,34 @@ export class LoginComponent extends BaseLoginComponent { } async submit() { - await this.stateService.setRememberEmail(this.rememberEmail); - if (!this.rememberEmail) { + const rememberEmail = this.formGroup.get("rememberEmail")?.value; + + await this.stateService.setRememberEmail(rememberEmail); + if (!rememberEmail) { await this.stateService.setRememberedEmail(null); } - await super.submit(); + await super.submit(false); + } + + async startPasswordlessLogin() { + this.formGroup.get("masterPassword")?.clearValidators(); + this.formGroup.get("masterPassword")?.updateValueAndValidity(); + + if (!this.formGroup.valid) { + return; + } + + const email = this.formGroup.get("email").value; + this.router.navigate(["/login-with-device"], { state: { email: email } }); } private getPasswordStrengthUserInput() { + const email = this.formGroup.get("email")?.value; let userInput: string[] = []; - const atPosition = this.email.indexOf("@"); + const atPosition = email.indexOf("@"); if (atPosition > -1) { userInput = userInput.concat( - this.email + email .substr(0, atPosition) .trim() .toLowerCase() diff --git a/apps/web/src/app/accounts/login/login.module.ts b/apps/web/src/app/accounts/login/login.module.ts new file mode 100644 index 00000000000..9ab8dfb3a1b --- /dev/null +++ b/apps/web/src/app/accounts/login/login.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from "@angular/core"; + +import { SharedModule } from "../../shared"; + +import { LoginWithDeviceComponent } from "./login-with-device.component"; +import { LoginComponent } from "./login.component"; + +@NgModule({ + imports: [SharedModule], + declarations: [LoginComponent, LoginWithDeviceComponent], + exports: [LoginComponent, LoginWithDeviceComponent], +}) +export class LoginModule {} diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 61daf7c82cb..6e1c4e569ef 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -11,7 +11,8 @@ import { AcceptEmergencyComponent } from "./accounts/accept-emergency.component" import { AcceptOrganizationComponent } from "./accounts/accept-organization.component"; import { HintComponent } from "./accounts/hint.component"; import { LockComponent } from "./accounts/lock.component"; -import { LoginComponent } from "./accounts/login.component"; +import { LoginWithDeviceComponent } from "./accounts/login/login-with-device.component"; +import { LoginComponent } from "./accounts/login/login.component"; import { RecoverDeleteComponent } from "./accounts/recover-delete.component"; import { RecoverTwoFactorComponent } from "./accounts/recover-two-factor.component"; import { RegisterComponent } from "./accounts/register.component"; @@ -60,6 +61,11 @@ const routes: Routes = [ canActivate: [HomeGuard], // Redirects either to vault, login or lock page. }, { path: "login", component: LoginComponent, canActivate: [UnauthGuard] }, + { + path: "login-with-device", + component: LoginWithDeviceComponent, + data: { titleId: "loginWithDevice" }, + }, { path: "2fa", component: TwoFactorComponent, canActivate: [UnauthGuard] }, { path: "register", diff --git a/apps/web/src/app/oss.module.ts b/apps/web/src/app/oss.module.ts index 0885d7d5d77..457200a0e9b 100644 --- a/apps/web/src/app/oss.module.ts +++ b/apps/web/src/app/oss.module.ts @@ -1,5 +1,6 @@ import { NgModule } from "@angular/core"; +import { LoginModule } from "./accounts/login/login.module"; import { TrialInitiationModule } from "./accounts/trial-initiation/trial-initiation.module"; import { OrganizationCreateModule } from "./organizations/create/organization-create.module"; import { OrganizationManageModule } from "./organizations/manage/organization-manage.module"; @@ -18,6 +19,7 @@ import { VaultFilterModule } from "./vault/vault-filter/vault-filter.module"; OrganizationManageModule, OrganizationUserModule, OrganizationCreateModule, + LoginModule, ], exports: [ SharedModule, @@ -25,6 +27,7 @@ import { VaultFilterModule } from "./vault/vault-filter/vault-filter.module"; TrialInitiationModule, VaultFilterModule, OrganizationBadgeModule, + LoginModule, ], bootstrap: [], }) diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 59315f9253c..c68741b5a2b 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -6,7 +6,6 @@ import { AcceptEmergencyComponent } from "../accounts/accept-emergency.component import { AcceptOrganizationComponent } from "../accounts/accept-organization.component"; import { HintComponent } from "../accounts/hint.component"; import { LockComponent } from "../accounts/lock.component"; -import { LoginComponent } from "../accounts/login.component"; import { RecoverDeleteComponent } from "../accounts/recover-delete.component"; import { RecoverTwoFactorComponent } from "../accounts/recover-two-factor.component"; import { RegisterFormModule } from "../accounts/register-form/register-form.module"; @@ -210,7 +209,6 @@ import { SharedModule } from "."; FrontendLayoutComponent, HintComponent, LockComponent, - LoginComponent, MasterPasswordPolicyComponent, NavbarComponent, NestedCheckboxComponent, @@ -355,7 +353,6 @@ import { SharedModule } from "."; FrontendLayoutComponent, HintComponent, LockComponent, - LoginComponent, MasterPasswordPolicyComponent, NavbarComponent, NestedCheckboxComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 80755183b9f..3b8c2f772d7 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -569,15 +569,27 @@ "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, + "loginWithDevice" : { + "message": "Log in with device" + }, + "loginWithDevciceEnabledInfo": { + "message": "Log in with device must be enabled in the settings of the Biwarden mobile app. Need another option?" + }, "createAccount": { "message": "Create Account" }, + "newAroundHere": { + "message": "New around here?" + }, "startTrial": { "message": "Start Trial" }, "logIn": { "message": "Log In" }, + "logInInitiated": { + "message": "Log in initiated" + }, "submit": { "message": "Submit" }, @@ -635,7 +647,7 @@ "confirmMasterPasswordRequired": { "message": "Master password retype is required." }, - "masterPasswordMinLength": { + "masterPasswordMinlength": { "message": "Master password must be at least 8 characters long." }, "masterPassDoesntMatch": { @@ -705,6 +717,9 @@ "noOrganizationsList": { "message": "You do not belong to any organizations. Organizations allow you to securely share items with other users." }, + "notificationSentDevice":{ + "message": "A notification has been sent to your device." + }, "versionNumber": { "message": "Version $VERSION_NUMBER$", "placeholders": { @@ -2532,6 +2547,9 @@ } } }, + "viewAllLoginOptions": { + "message": "View all log in options" + }, "viewedItemId": { "message": "Viewed item $ID$.", "placeholders": { @@ -3372,6 +3390,12 @@ "message": "To ensure the integrity of your encryption keys, please verify the user's fingerprint phrase before continuing.", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, + "fingerprintMatchInfo": { + "message": "Please make sure your vault is unlocked and Fingerprint phrase matches the other device." + }, + "fingerprintPhraseHeader": { + "message": "Fingerprint phrase" + }, "dontAskFingerprintAgain": { "message": "Never prompt to verify fingerprint phrases for invited users (Not recommended)", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -4372,6 +4396,9 @@ "reinviteSelected": { "message": "Resend Invitations" }, + "resendNotification": { + "message": "Resend notification" + }, "noSelectedUsersApplicable": { "message": "This action is not applicable to any of the selected users." }, diff --git a/apps/web/src/utils/flags.ts b/apps/web/src/utils/flags.ts index 5cc3b930bb4..195bc8e5f54 100644 --- a/apps/web/src/utils/flags.ts +++ b/apps/web/src/utils/flags.ts @@ -10,6 +10,7 @@ import { /* eslint-disable-next-line @typescript-eslint/ban-types */ export type Flags = { showTrial?: boolean; + showPasswordless?: boolean; } & SharedFlags; // required to avoid linting errors when there are no flags diff --git a/libs/angular/src/components/login.component.ts b/libs/angular/src/components/login.component.ts index 1c7a8c2332d..1bc2e8ed871 100644 --- a/libs/angular/src/components/login.component.ts +++ b/libs/angular/src/components/login.component.ts @@ -1,10 +1,15 @@ -import { Directive, Input, NgZone, OnInit } from "@angular/core"; +import { Directive, NgZone, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; import { take } from "rxjs/operators"; 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 { + AllValidationErrors, + FormValidationErrorsService, +} from "@bitwarden/common/abstractions/formValidationErrors.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; @@ -18,16 +23,19 @@ import { CaptchaProtectedComponent } from "./captchaProtected.component"; @Directive() export class LoginComponent extends CaptchaProtectedComponent implements OnInit { - @Input() email = ""; - @Input() rememberEmail = true; - - masterPassword = ""; showPassword = false; formPromise: Promise; onSuccessfulLogin: () => Promise; onSuccessfulLoginNavigate: () => Promise; onSuccessfulLoginTwoFactorNavigate: () => Promise; onSuccessfulLoginForceResetNavigate: () => Promise; + selfHosted = false; + + formGroup = this.formBuilder.group({ + email: ["", [Validators.required, Validators.email]], + masterPassword: ["", [Validators.required, Validators.minLength(8)]], + rememberEmail: [true], + }); protected twoFactorRoute = "2fa"; protected successRoute = "vault"; @@ -44,9 +52,12 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit protected passwordGenerationService: PasswordGenerationService, protected cryptoFunctionService: CryptoFunctionService, protected logService: LogService, - protected ngZone: NgZone + protected ngZone: NgZone, + protected formBuilder: FormBuilder, + protected formValidationErrorService: FormValidationErrorsService ) { super(environmentService, i18nService, platformUtilsService); + this.selfHosted = platformUtilsService.isSelfHost(); } get selfHostedDomain() { @@ -54,59 +65,53 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit } async ngOnInit() { - if (this.email == null || this.email === "") { - this.email = await this.stateService.getRememberedEmail(); - if (this.email == null) { - this.email = ""; + let email = this.formGroup.get("email")?.value; + 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.alwaysRememberEmail) { - this.rememberEmail = (await this.stateService.getRememberedEmail()) != null; - } - if (Utils.isBrowser && !Utils.isNode) { - this.focusInput(); + const rememberEmail = (await this.stateService.getRememberedEmail()) != null; + this.formGroup.get("rememberEmail")?.setValue(rememberEmail); } } - async submit() { + async submit(showToast = true) { + const email = this.formGroup.get("email")?.value; + const masterPassword = this.formGroup.get("masterPassword")?.value; + const rememberEmail = this.formGroup.get("rememberEmail")?.value; + await this.setupCaptcha(); - if (this.email == null || this.email === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("emailRequired") - ); - return; - } - if (this.email.indexOf("@") === -1) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidEmail") - ); + this.formGroup.markAllAsTouched(); + + //web + if (this.formGroup.invalid && !showToast) { return; } - if (this.masterPassword == null || this.masterPassword === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordRequired") - ); + + //desktop, browser; This should be removed once all clients use reactive forms + if (this.formGroup.invalid && showToast) { + const errorText = this.getErrorToastMessage(); + this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText); return; } try { const credentials = new PasswordLogInCredentials( - this.email, - this.masterPassword, + email, + masterPassword, this.captchaToken, null ); this.formPromise = this.authService.logIn(credentials); const response = await this.formPromise; - if (this.rememberEmail || this.alwaysRememberEmail) { - await this.stateService.setRememberedEmail(this.email); + if (rememberEmail || this.alwaysRememberEmail) { + await this.stateService.setRememberedEmail(email); } else { await this.stateService.setRememberedEmail(null); } @@ -188,9 +193,30 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit ); } + private getErrorToastMessage() { + const error: AllValidationErrors = this.formValidationErrorService + .getFormValidationErrors(this.formGroup.controls) + .shift(); + + if (error) { + switch (error.errorName) { + case "email": + return this.i18nService.t("invalidEmail"); + default: + return this.i18nService.t(this.errorTag(error)); + } + } + + return; + } + + private errorTag(error: AllValidationErrors): string { + const name = error.errorName.charAt(0).toUpperCase() + error.errorName.slice(1); + return `${error.controlName}${name}`; + } + protected focusInput() { - document - .getElementById(this.email == null || this.email === "" ? "email" : "masterPassword") - .focus(); + const email = this.formGroup.get("email")?.value; + document.getElementById(email == null || email === "" ? "email" : "masterPassword").focus(); } } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b76ad9a5903..b53aac0866a 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -5,6 +5,7 @@ import { AbstractThemingService } from "@bitwarden/angular/services/theming/them import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/abstractions/account/account-api.service.abstraction"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/abstractions/account/account.service.abstraction"; +import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/abstractions/anonymousHub.service"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/abstractions/appId.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -62,6 +63,7 @@ import { Account } from "@bitwarden/common/models/domain/account"; import { GlobalState } from "@bitwarden/common/models/domain/globalState"; import { AccountApiService } from "@bitwarden/common/services/account/account-api.service"; import { AccountService } from "@bitwarden/common/services/account/account.service"; +import { AnonymousHubService } from "@bitwarden/common/services/anonymousHub.service"; import { ApiService } from "@bitwarden/common/services/api.service"; import { AppIdService } from "@bitwarden/common/services/appId.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; @@ -544,6 +546,11 @@ import { ValidationService } from "./validation.service"; useClass: ConfigApiService, deps: [ApiServiceAbstraction], }, + { + provide: AnonymousHubServiceAbstraction, + useClass: AnonymousHubService, + deps: [EnvironmentServiceAbstraction, AuthServiceAbstraction, LogService], + }, ], }) export class JslibServicesModule {} diff --git a/libs/common/src/abstractions/anonymousHub.service.ts b/libs/common/src/abstractions/anonymousHub.service.ts new file mode 100644 index 00000000000..43bdabd512c --- /dev/null +++ b/libs/common/src/abstractions/anonymousHub.service.ts @@ -0,0 +1,4 @@ +export abstract class AnonymousHubService { + createHubConnection: (token: string) => void; + stopHubConnection: () => void; +} diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index f08e5c34af8..06d3f5b4eba 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -46,6 +46,7 @@ import { OrganizationUserUpdateGroupsRequest } from "../models/request/organizat import { OrganizationUserUpdateRequest } from "../models/request/organizationUserUpdateRequest"; import { PasswordHintRequest } from "../models/request/passwordHintRequest"; import { PasswordRequest } from "../models/request/passwordRequest"; +import { PasswordlessCreateAuthRequest } from "../models/request/passwordlessCreateAuthRequest"; import { PaymentRequest } from "../models/request/paymentRequest"; import { PreloginRequest } from "../models/request/preloginRequest"; import { ProviderAddOrganizationRequest } from "../models/request/provider/providerAddOrganizationRequest"; @@ -84,6 +85,7 @@ import { VerifyEmailRequest } from "../models/request/verifyEmailRequest"; import { ApiKeyResponse } from "../models/response/apiKeyResponse"; import { AttachmentResponse } from "../models/response/attachmentResponse"; import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse"; +import { AuthRequestResponse } from "../models/response/authRequestResponse"; import { RegisterResponse } from "../models/response/authentication/registerResponse"; import { BillingHistoryResponse } from "../models/response/billingHistoryResponse"; import { BillingPaymentResponse } from "../models/response/billingPaymentResponse"; @@ -210,6 +212,9 @@ export abstract class ApiService { postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise; putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise; postConvertToKeyConnector: () => Promise; + //passwordless + postAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise; + getAuthResponse: (id: string, accessCode: string) => Promise; getUserBillingHistory: () => Promise; getUserBillingPayment: () => Promise; diff --git a/libs/common/src/abstractions/auth.service.ts b/libs/common/src/abstractions/auth.service.ts index 4947f21708b..bbe1c01bf29 100644 --- a/libs/common/src/abstractions/auth.service.ts +++ b/libs/common/src/abstractions/auth.service.ts @@ -1,18 +1,26 @@ +import { Observable } from "rxjs"; + import { AuthenticationStatus } from "../enums/authenticationStatus"; import { AuthResult } from "../models/domain/authResult"; import { ApiLogInCredentials, PasswordLogInCredentials, SsoLogInCredentials, + PasswordlessLogInCredentials, } from "../models/domain/logInCredentials"; import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor"; +import { AuthRequestPushNotification } from "../models/response/notificationResponse"; export abstract class AuthService { masterPasswordHash: string; email: string; logIn: ( - credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials + credentials: + | ApiLogInCredentials + | PasswordLogInCredentials + | SsoLogInCredentials + | PasswordlessLogInCredentials ) => Promise; logInTwoFactor: ( twoFactor: TokenRequestTwoFactor, @@ -24,4 +32,7 @@ export abstract class AuthService { authingWithSso: () => boolean; authingWithPassword: () => boolean; getAuthStatus: (userId?: string) => Promise; + authResponsePushNotifiction: (notification: AuthRequestPushNotification) => Promise; + + getPushNotifcationObs$: () => Observable; } diff --git a/libs/common/src/enums/authRequestType.ts b/libs/common/src/enums/authRequestType.ts new file mode 100644 index 00000000000..4edfa5b8889 --- /dev/null +++ b/libs/common/src/enums/authRequestType.ts @@ -0,0 +1,4 @@ +export enum AuthRequestType { + AuthenticateAndUnlock = 0, + Unlock = 1, +} diff --git a/libs/common/src/enums/authenticationType.ts b/libs/common/src/enums/authenticationType.ts index ed7375c8085..5133c4f648e 100644 --- a/libs/common/src/enums/authenticationType.ts +++ b/libs/common/src/enums/authenticationType.ts @@ -2,4 +2,5 @@ export enum AuthenticationType { Password = 0, Sso = 1, Api = 2, + Passwordless = 3, } diff --git a/libs/common/src/enums/notificationType.ts b/libs/common/src/enums/notificationType.ts index 77ebde01fc5..457ad174cad 100644 --- a/libs/common/src/enums/notificationType.ts +++ b/libs/common/src/enums/notificationType.ts @@ -17,4 +17,7 @@ export enum NotificationType { SyncSendCreate = 12, SyncSendUpdate = 13, SyncSendDelete = 14, + + AuthRequest = 15, + AuthRequestResponse = 16, } diff --git a/libs/common/src/misc/logInStrategies/logIn.strategy.ts b/libs/common/src/misc/logInStrategies/logIn.strategy.ts index 8615700681b..577130156f7 100644 --- a/libs/common/src/misc/logInStrategies/logIn.strategy.ts +++ b/libs/common/src/misc/logInStrategies/logIn.strategy.ts @@ -14,6 +14,7 @@ import { ApiLogInCredentials, PasswordLogInCredentials, SsoLogInCredentials, + PasswordlessLogInCredentials, } from "../../models/domain/logInCredentials"; import { DeviceRequest } from "../../models/request/deviceRequest"; import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest"; @@ -42,7 +43,11 @@ export abstract class LogInStrategy { ) {} abstract logIn( - credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials + credentials: + | ApiLogInCredentials + | PasswordLogInCredentials + | SsoLogInCredentials + | PasswordlessLogInCredentials ): Promise; async logInTwoFactor( diff --git a/libs/common/src/misc/logInStrategies/passwordlessLogin.strategy.ts b/libs/common/src/misc/logInStrategies/passwordlessLogin.strategy.ts new file mode 100644 index 00000000000..0acc4a49f00 --- /dev/null +++ b/libs/common/src/misc/logInStrategies/passwordlessLogin.strategy.ts @@ -0,0 +1,86 @@ +import { ApiService } from "../../abstractions/api.service"; +import { AppIdService } from "../../abstractions/appId.service"; +import { AuthService } from "../../abstractions/auth.service"; +import { CryptoService } from "../../abstractions/crypto.service"; +import { LogService } from "../../abstractions/log.service"; +import { MessagingService } from "../../abstractions/messaging.service"; +import { PlatformUtilsService } from "../../abstractions/platformUtils.service"; +import { StateService } from "../../abstractions/state.service"; +import { TokenService } from "../../abstractions/token.service"; +import { TwoFactorService } from "../../abstractions/twoFactor.service"; +import { AuthResult } from "../../models/domain/authResult"; +import { PasswordlessLogInCredentials } from "../../models/domain/logInCredentials"; +import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey"; +import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest"; +import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequestTwoFactor"; + +import { LogInStrategy } from "./logIn.strategy"; + +export class PasswordlessLogInStrategy extends LogInStrategy { + get email() { + return this.tokenRequest.email; + } + + get masterPasswordHash() { + return this.tokenRequest.masterPasswordHash; + } + + tokenRequest: PasswordTokenRequest; + + private localHashedPassword: string; + private key: SymmetricCryptoKey; + + constructor( + cryptoService: CryptoService, + apiService: ApiService, + tokenService: TokenService, + appIdService: AppIdService, + platformUtilsService: PlatformUtilsService, + messagingService: MessagingService, + logService: LogService, + stateService: StateService, + twoFactorService: TwoFactorService, + private authService: AuthService + ) { + super( + cryptoService, + apiService, + tokenService, + appIdService, + platformUtilsService, + messagingService, + logService, + stateService, + twoFactorService + ); + } + + async onSuccessfulLogin() { + await this.cryptoService.setKey(this.key); + await this.cryptoService.setKeyHash(this.localHashedPassword); + } + + async logInTwoFactor( + twoFactor: TokenRequestTwoFactor, + captchaResponse: string + ): Promise { + this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken; + return super.logInTwoFactor(twoFactor); + } + + async logIn(credentials: PasswordlessLogInCredentials) { + this.localHashedPassword = credentials.localPasswordHash; + this.key = credentials.decKey; + + this.tokenRequest = new PasswordTokenRequest( + credentials.email, + credentials.accessCode, + null, + await this.buildTwoFactor(credentials.twoFactor), + await this.buildDeviceRequest() + ); + + this.tokenRequest.setPasswordlessAccessCode(credentials.authRequestId); + return this.startLogIn(); + } +} diff --git a/libs/common/src/models/domain/logInCredentials.ts b/libs/common/src/models/domain/logInCredentials.ts index c1e23610e47..5f2035fd156 100644 --- a/libs/common/src/models/domain/logInCredentials.ts +++ b/libs/common/src/models/domain/logInCredentials.ts @@ -1,3 +1,5 @@ +import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; + import { AuthenticationType } from "../../enums/authenticationType"; import { TokenRequestTwoFactor } from "../request/identityToken/tokenRequestTwoFactor"; @@ -29,3 +31,16 @@ export class ApiLogInCredentials { constructor(public clientId: string, public clientSecret: string) {} } + +export class PasswordlessLogInCredentials { + readonly type = AuthenticationType.Passwordless; + + constructor( + public email: string, + public accessCode: string, + public authRequestId: string, + public decKey: SymmetricCryptoKey, + public localPasswordHash: string, + public twoFactor?: TokenRequestTwoFactor + ) {} +} diff --git a/libs/common/src/models/request/identityToken/tokenRequest.ts b/libs/common/src/models/request/identityToken/tokenRequest.ts index 82a4a394c5d..5e38d2069b0 100644 --- a/libs/common/src/models/request/identityToken/tokenRequest.ts +++ b/libs/common/src/models/request/identityToken/tokenRequest.ts @@ -4,6 +4,7 @@ import { TokenRequestTwoFactor } from "./tokenRequestTwoFactor"; export abstract class TokenRequest { protected device?: DeviceRequest; + protected passwordlessAuthRequest: string; constructor(protected twoFactor: TokenRequestTwoFactor, device?: DeviceRequest) { this.device = device != null ? device : null; @@ -18,6 +19,10 @@ export abstract class TokenRequest { this.twoFactor = twoFactor; } + setPasswordlessAccessCode(accessCode: string) { + this.passwordlessAuthRequest = accessCode; + } + protected toIdentityToken(clientId: string) { const obj: any = { scope: "api offline_access", @@ -32,6 +37,11 @@ export abstract class TokenRequest { // obj.devicePushToken = this.device.pushToken; } + //passswordless login + if (this.passwordlessAuthRequest) { + obj.authRequest = this.passwordlessAuthRequest; + } + if (this.twoFactor.token && this.twoFactor.provider != null) { obj.twoFactorToken = this.twoFactor.token; obj.twoFactorProvider = this.twoFactor.provider; diff --git a/libs/common/src/models/request/passwordlessCreateAuthRequest.ts b/libs/common/src/models/request/passwordlessCreateAuthRequest.ts new file mode 100644 index 00000000000..df83c547775 --- /dev/null +++ b/libs/common/src/models/request/passwordlessCreateAuthRequest.ts @@ -0,0 +1,12 @@ +import { AuthRequestType } from "../../enums/authRequestType"; + +export class PasswordlessCreateAuthRequest { + constructor( + readonly email: string, + readonly deviceIdentifier: string, + readonly publicKey: string, + readonly type: AuthRequestType, + readonly accessCode: string, + readonly fingerprintPhrase: string + ) {} +} diff --git a/libs/common/src/models/response/authRequestResponse.ts b/libs/common/src/models/response/authRequestResponse.ts new file mode 100644 index 00000000000..1a29a3da856 --- /dev/null +++ b/libs/common/src/models/response/authRequestResponse.ts @@ -0,0 +1,26 @@ +import { DeviceType } from "@bitwarden/common/enums/deviceType"; + +import { BaseResponse } from "./baseResponse"; + +export class AuthRequestResponse extends BaseResponse { + id: string; + publicKey: string; + requestDeviceType: DeviceType; + requestIpAddress: string; + key: string; + masterPasswordHash: string; + creationDate: string; + requestApproved: boolean; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("Id"); + this.publicKey = this.getResponseProperty("PublicKey"); + this.requestDeviceType = this.getResponseProperty("RequestDeviceType"); + this.requestIpAddress = this.getResponseProperty("RequestIpAddress"); + this.key = this.getResponseProperty("Key"); + this.masterPasswordHash = this.getResponseProperty("MasterPasswordHash"); + this.creationDate = this.getResponseProperty("CreationDate"); + this.requestApproved = this.getResponseProperty("RequestApproved"); + } +} diff --git a/libs/common/src/models/response/notificationResponse.ts b/libs/common/src/models/response/notificationResponse.ts index f23de8fe8be..1e2a5045063 100644 --- a/libs/common/src/models/response/notificationResponse.ts +++ b/libs/common/src/models/response/notificationResponse.ts @@ -37,6 +37,10 @@ export class NotificationResponse extends BaseResponse { case NotificationType.SyncSendDelete: this.payload = new SyncSendNotification(payload); break; + case NotificationType.AuthRequest: + case NotificationType.AuthRequestResponse: + this.payload = new AuthRequestPushNotification(payload); + break; default: break; } @@ -96,3 +100,14 @@ export class SyncSendNotification extends BaseResponse { this.revisionDate = new Date(this.getResponseProperty("RevisionDate")); } } + +export class AuthRequestPushNotification extends BaseResponse { + id: string; + userId: string; + + constructor(response: any) { + super(response); + this.id = this.getResponseProperty("Id"); + this.userId = this.getResponseProperty("UserId"); + } +} diff --git a/libs/common/src/services/anonymousHub.service.ts b/libs/common/src/services/anonymousHub.service.ts new file mode 100644 index 00000000000..13b5898b18b --- /dev/null +++ b/libs/common/src/services/anonymousHub.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from "@angular/core"; +import { + HttpTransportType, + HubConnection, + HubConnectionBuilder, + IHubProtocol, +} from "@microsoft/signalr"; +import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; + +import { AnonymousHubService as AnonymousHubServiceAbstraction } from "../abstractions/anonymousHub.service"; +import { AuthService } from "../abstractions/auth.service"; +import { EnvironmentService } from "../abstractions/environment.service"; +import { LogService } from "../abstractions/log.service"; + +import { + AuthRequestPushNotification, + NotificationResponse, +} from "./../models/response/notificationResponse"; + +@Injectable() +export class AnonymousHubService implements AnonymousHubServiceAbstraction { + private anonHubConnection: HubConnection; + private url: string; + + constructor( + private environmentService: EnvironmentService, + private authService: AuthService, + private logService: LogService + ) {} + + async createHubConnection(token: string) { + this.url = this.environmentService.getNotificationsUrl(); + + this.anonHubConnection = new HubConnectionBuilder() + .withUrl(this.url + "/anonymousHub?Token=" + token, { + skipNegotiation: true, + transport: HttpTransportType.WebSockets, + }) + .withHubProtocol(new MessagePackHubProtocol() as IHubProtocol) + .build(); + + this.anonHubConnection.start().catch((error) => this.logService.error(error)); + + this.anonHubConnection.on("AuthRequestResponseRecieved", (data: any) => { + this.ProcessNotification(new NotificationResponse(data)); + }); + } + + stopHubConnection() { + if (this.anonHubConnection) { + this.anonHubConnection.stop(); + } + } + + private async ProcessNotification(notification: NotificationResponse) { + await this.authService.authResponsePushNotifiction( + notification.payload as AuthRequestPushNotification + ); + } +} diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 0b96a11831c..a4648e25045 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -54,6 +54,7 @@ import { OrganizationUserUpdateGroupsRequest } from "../models/request/organizat import { OrganizationUserUpdateRequest } from "../models/request/organizationUserUpdateRequest"; import { PasswordHintRequest } from "../models/request/passwordHintRequest"; import { PasswordRequest } from "../models/request/passwordRequest"; +import { PasswordlessCreateAuthRequest } from "../models/request/passwordlessCreateAuthRequest"; import { PaymentRequest } from "../models/request/paymentRequest"; import { PreloginRequest } from "../models/request/preloginRequest"; import { ProviderAddOrganizationRequest } from "../models/request/provider/providerAddOrganizationRequest"; @@ -92,6 +93,7 @@ import { VerifyEmailRequest } from "../models/request/verifyEmailRequest"; import { ApiKeyResponse } from "../models/response/apiKeyResponse"; import { AttachmentResponse } from "../models/response/attachmentResponse"; import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse"; +import { AuthRequestResponse } from "../models/response/authRequestResponse"; import { RegisterResponse } from "../models/response/authentication/registerResponse"; import { BillingHistoryResponse } from "../models/response/billingHistoryResponse"; import { BillingPaymentResponse } from "../models/response/billingPaymentResponse"; @@ -265,6 +267,17 @@ export class ApiService implements ApiServiceAbstraction { } } + async postAuthRequest(request: PasswordlessCreateAuthRequest): Promise { + const r = await this.send("POST", "/auth-requests/", request, false, true); + return new AuthRequestResponse(r); + } + + async getAuthResponse(id: string, accessCode: string): Promise { + const path = `/auth-requests/${id}/response?code=${accessCode}`; + const r = await this.send("GET", path, null, false, true); + return new AuthRequestResponse(r); + } + // Account APIs async getProfile(): Promise { diff --git a/libs/common/src/services/auth.service.ts b/libs/common/src/services/auth.service.ts index 6f77bca20b3..3807eee3d69 100644 --- a/libs/common/src/services/auth.service.ts +++ b/libs/common/src/services/auth.service.ts @@ -1,3 +1,5 @@ +import { Observable, Subject } from "rxjs"; + import { ApiService } from "../abstractions/api.service"; import { AppIdService } from "../abstractions/appId.service"; import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service"; @@ -17,17 +19,20 @@ import { KdfType } from "../enums/kdfType"; import { KeySuffixOptions } from "../enums/keySuffixOptions"; import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy"; import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy"; +import { PasswordlessLogInStrategy } from "../misc/logInStrategies/passwordlessLogin.strategy"; import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy"; import { AuthResult } from "../models/domain/authResult"; import { ApiLogInCredentials, PasswordLogInCredentials, SsoLogInCredentials, + PasswordlessLogInCredentials, } from "../models/domain/logInCredentials"; import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor"; import { PreloginRequest } from "../models/request/preloginRequest"; import { ErrorResponse } from "../models/response/errorResponse"; +import { AuthRequestPushNotification } from "../models/response/notificationResponse"; const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes @@ -42,9 +47,15 @@ export class AuthService implements AuthServiceAbstraction { : null; } - private logInStrategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy; + private logInStrategy: + | ApiLogInStrategy + | PasswordLogInStrategy + | SsoLogInStrategy + | PasswordlessLogInStrategy; private sessionTimeout: any; + private pushNotificationSubject = new Subject(); + constructor( protected cryptoService: CryptoService, protected apiService: ApiService, @@ -61,52 +72,78 @@ export class AuthService implements AuthServiceAbstraction { ) {} async logIn( - credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials + credentials: + | ApiLogInCredentials + | PasswordLogInCredentials + | SsoLogInCredentials + | PasswordlessLogInCredentials ): Promise { this.clearState(); - let strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy; - - if (credentials.type === AuthenticationType.Password) { - strategy = new PasswordLogInStrategy( - this.cryptoService, - this.apiService, - this.tokenService, - this.appIdService, - this.platformUtilsService, - this.messagingService, - this.logService, - this.stateService, - this.twoFactorService, - this - ); - } else if (credentials.type === AuthenticationType.Sso) { - strategy = new SsoLogInStrategy( - this.cryptoService, - this.apiService, - this.tokenService, - this.appIdService, - this.platformUtilsService, - this.messagingService, - this.logService, - this.stateService, - this.twoFactorService, - this.keyConnectorService - ); - } else if (credentials.type === AuthenticationType.Api) { - strategy = new ApiLogInStrategy( - this.cryptoService, - this.apiService, - this.tokenService, - this.appIdService, - this.platformUtilsService, - this.messagingService, - this.logService, - this.stateService, - this.twoFactorService, - this.environmentService, - this.keyConnectorService - ); + let strategy: + | ApiLogInStrategy + | PasswordLogInStrategy + | SsoLogInStrategy + | PasswordlessLogInStrategy; + + switch (credentials.type) { + case AuthenticationType.Password: + strategy = new PasswordLogInStrategy( + this.cryptoService, + this.apiService, + this.tokenService, + this.appIdService, + this.platformUtilsService, + this.messagingService, + this.logService, + this.stateService, + this.twoFactorService, + this + ); + break; + case AuthenticationType.Sso: + strategy = new SsoLogInStrategy( + this.cryptoService, + this.apiService, + this.tokenService, + this.appIdService, + this.platformUtilsService, + this.messagingService, + this.logService, + this.stateService, + this.twoFactorService, + this.keyConnectorService + ); + break; + case AuthenticationType.Api: + strategy = new ApiLogInStrategy( + this.cryptoService, + this.apiService, + this.tokenService, + this.appIdService, + this.platformUtilsService, + this.messagingService, + this.logService, + this.stateService, + this.twoFactorService, + this.environmentService, + this.keyConnectorService + ); + break; + case AuthenticationType.Passwordless: + strategy = new PasswordlessLogInStrategy( + this.cryptoService, + this.apiService, + this.tokenService, + this.appIdService, + this.platformUtilsService, + this.messagingService, + this.logService, + this.stateService, + this.twoFactorService, + this + ); + break; } const result = await strategy.logIn(credentials as any); @@ -202,7 +239,21 @@ export class AuthService implements AuthServiceAbstraction { return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations); } - private saveState(strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy) { + async authResponsePushNotifiction(notification: AuthRequestPushNotification): Promise { + this.pushNotificationSubject.next(notification.id); + } + + getPushNotifcationObs$(): Observable { + return this.pushNotificationSubject.asObservable(); + } + + private saveState( + strategy: + | ApiLogInStrategy + | PasswordLogInStrategy + | SsoLogInStrategy + | PasswordlessLogInStrategy + ) { this.logInStrategy = strategy; this.startSessionTimeout(); } From 1eec3c909de2558f766dfdfb6b7c55bd35d11e9b Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Tue, 27 Sep 2022 11:09:48 -0400 Subject: [PATCH 02/20] Fix last seen & spacing issue (#3567) * Fix last seen & spacing issue * Address PR comments, moving try catch to config service (cherry picked from commit d168d5ee9b2525c3290e2f65e6de8337eff9fbe2) --- apps/browser/src/_locales/en/messages.json | 2 +- .../src/popup/settings/about.component.html | 6 +++--- .../src/services/config/config.service.ts | 18 ++++++++++-------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index a23bfa56501..3749bb4ae00 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2017,7 +2017,7 @@ } }, "lastSeenOn": { - "message": "last seen on $DATE$", + "message": "last seen on: $DATE$", "placeholders": { "date": { "content": "$1", diff --git a/apps/browser/src/popup/settings/about.component.html b/apps/browser/src/popup/settings/about.component.html index ed2ceb40bec..c11b68e9c1f 100644 --- a/apps/browser/src/popup/settings/about.component.html +++ b/apps/browser/src/popup/settings/about.component.html @@ -14,7 +14,7 @@

{{ "serverVersion" | i18n }}: {{ this.serverConfig?.version }} - ({{ "lastSeenOn" | i18n }}: {{ serverConfig.utcDate | date: "mediumDate" }}) + ({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})

@@ -24,7 +24,7 @@ {{ "serverVersion" | i18n }} ({{ "thirdParty" | i18n }}): {{ this.serverConfig?.version }} - ({{ "lastSeenOn" | i18n }}: {{ serverConfig.utcDate | date: "mediumDate" }}) + ({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})

@@ -36,7 +36,7 @@ {{ "serverVersion" | i18n }} ({{ "selfHosted" | i18n }}): {{ this.serverConfig?.version }} - ({{ "lastSeenOn" | i18n }}: {{ serverConfig.utcDate | date: "mediumDate" }}) + ({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})

diff --git a/libs/common/src/services/config/config.service.ts b/libs/common/src/services/config/config.service.ts index 9f9e0938b5a..a5ac5445713 100644 --- a/libs/common/src/services/config/config.service.ts +++ b/libs/common/src/services/config/config.service.ts @@ -48,14 +48,16 @@ export class ConfigService implements ConfigServiceAbstraction { } private async fetchServerConfig(): Promise { - const response = await this.configApiService.get(); - const data = new ServerConfigData(response); - - if (data != null) { - await this.stateService.setServerConfig(data); - return new ServerConfig(data); + try { + const response = await this.configApiService.get(); + + if (response != null) { + const data = new ServerConfigData(response); + await this.stateService.setServerConfig(data); + return new ServerConfig(data); + } + } catch { + return null; } - - return null; } } From 0737c654c3f69f42a43735c38ba59f819688b33e Mon Sep 17 00:00:00 2001 From: Gbubemi Smith Date: Tue, 27 Sep 2022 19:10:19 +0100 Subject: [PATCH 03/20] corrected typos (#3633) (cherry picked from commit 2c68518f875518257e87dd8bf6e47c4f191950a2) --- .../src/app/accounts/login/login-with-device.component.html | 2 +- apps/web/src/locales/en/messages.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/accounts/login/login-with-device.component.html b/apps/web/src/app/accounts/login/login-with-device.component.html index 3105a639ad3..3e5f48d597c 100644 --- a/apps/web/src/app/accounts/login/login-with-device.component.html +++ b/apps/web/src/app/accounts/login/login-with-device.component.html @@ -36,7 +36,7 @@
- {{ "loginWithDevciceEnabledInfo" | i18n }} + {{ "loginWithDeviceEnabledInfo" | i18n }} {{ "viewAllLoginOptions" | i18n }}
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 3b8c2f772d7..efced93ca43 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -572,8 +572,8 @@ "loginWithDevice" : { "message": "Log in with device" }, - "loginWithDevciceEnabledInfo": { - "message": "Log in with device must be enabled in the settings of the Biwarden mobile app. Need another option?" + "loginWithDeviceEnabledInfo": { + "message": "Log in with device must be enabled in the settings of the Bitwarden mobile app. Need another option?" }, "createAccount": { "message": "Create Account" From da556d21a53d85bc87f005a35a8222d78d121b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Wed, 28 Sep 2022 01:19:55 +0200 Subject: [PATCH 04/20] Fix web qa deployment (#3632) * Comment unzip downloaded artifact * Replace gihub ref with master * Add debug commands * Change CP command * Add another debug code * Fix * Another fix * Remove debug steps * Remove master const branch after testing (cherry picked from commit c243f3594e9795c3fedc7aab20c63cd8fdbc4c9b) --- .github/workflows/release-qa-web.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/release-qa-web.yml b/.github/workflows/release-qa-web.yml index 1e31c95cbeb..f02286be2f7 100644 --- a/.github/workflows/release-qa-web.yml +++ b/.github/workflows/release-qa-web.yml @@ -34,15 +34,11 @@ jobs: uses: bitwarden/gh-actions/download-artifacts@850faad0cf6c02a8c0dc46eddde2363fbd6c373a with: workflow: build-web.yml - path: apps/web + path: apps/web/build workflow_conclusion: success branch: ${{ github.ref_name }} artifacts: web-*-cloud-QA.zip - # This should result in a build directory in the current working directory - - name: Unzip build asset - working-directory: apps/web - run: unzip web-*-cloud-QA.zip - name: Checkout Repo uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v3.0.2 From 329b9bb9fa5820253e8021428c232b6c1a89ec16 Mon Sep 17 00:00:00 2001 From: Gbubemi Smith Date: Wed, 28 Sep 2022 18:23:48 +0100 Subject: [PATCH 05/20] updated submit button to use the block directive and add loader (#3644) (cherry picked from commit 9c40ac4e14f784a96981ee77c78773c30191d0b9) --- apps/web/src/app/accounts/login/login.component.html | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/web/src/app/accounts/login/login.component.html b/apps/web/src/app/accounts/login/login.component.html index 7df9777f39c..6152b585953 100644 --- a/apps/web/src/app/accounts/login/login.component.html +++ b/apps/web/src/app/accounts/login/login.component.html @@ -79,18 +79,14 @@ bitButton buttonType="primary" type="submit" - class="tw-inline-block tw-w-1/2" + [block]="true" + [loading]="form.loading" [disabled]="form.loading" > {{ "logIn" | i18n }} - + {{ "createAccount" | i18n }} From 006e430e2572aa6a255c08e18fa5eb86c208186b Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 29 Sep 2022 08:55:51 +1000 Subject: [PATCH 06/20] Fix alignment for long filter names (#3635) (cherry picked from commit 24a7abbba70aa434827e7b009a23dc9e7d2dcf48) --- apps/desktop/src/scss/left-nav.scss | 4 ++++ apps/web/src/scss/vault-filters.scss | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/scss/left-nav.scss b/apps/desktop/src/scss/left-nav.scss index 5b52d78291a..dc882ad265d 100644 --- a/apps/desktop/src/scss/left-nav.scss +++ b/apps/desktop/src/scss/left-nav.scss @@ -154,6 +154,10 @@ margin-left: auto; margin-right: 5px; } + + .filter-button { + white-space: nowrap; + } } .nav { diff --git a/apps/web/src/scss/vault-filters.scss b/apps/web/src/scss/vault-filters.scss index 1925de3d9af..9f39164ad7b 100644 --- a/apps/web/src/scss/vault-filters.scss +++ b/apps/web/src/scss/vault-filters.scss @@ -104,6 +104,9 @@ } .filter-button { + max-width: 90%; + white-space: nowrap; + &:hover, &:focus, &:active { @@ -112,7 +115,6 @@ } text-decoration: none; } - max-width: 90%; &.disabled-organization { max-width: 78%; From 4a102f4cbe2f9ba43d21706e3577b4215fecbe5f Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 27 Sep 2022 11:25:53 +0200 Subject: [PATCH 07/20] [EC-556] refactor cl button (#3537) * [EC-556] feat: convert button into component * [EC-556] feat: implement loading state * [EC-556] feat: remove loading from submit button * [EC-556] fix: add missing import * [EC-556] fix: disabling button using regular attribute * [EC-556] fix: missing loading input in story templates * [EC-556] feat: remove and replace submit button * Fix packaging on Build Web workflow (#3613) (cherry picked from commit 67c447d54ce0f08b99e2efc63b28cafac3411486) * [EC-556] fix: replaced buttons should be primary Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> (cherry picked from commit cd7c9bfd9f45754057671c21a53b5da8db0d40f7) --- .../register-form.component.html | 8 ++- .../trial-initiation/billing.component.html | 4 +- .../settings/account.component.html | 4 +- ...nroll-master-password-reset.component.html | 4 +- .../pages/breach-report.component.html | 4 +- .../exposed-passwords-report.component.html | 4 +- .../app/settings/change-kdf.component.html | 4 +- .../settings/change-password.component.html | 4 +- .../emergency-access-add-edit.component.html | 10 +++- .../organization-plans.component.html | 12 +++- .../src/app/settings/premium.component.html | 8 +-- .../settings/two-factor-setup.component.html | 10 +++- apps/web/src/app/shared/shared.module.ts | 3 - .../organizations/manage/scim.component.html | 10 +++- .../src/button/button.component.html | 8 +++ ...ctive.spec.ts => button.component.spec.ts} | 57 ++++++++++++++++++- ...utton.directive.ts => button.component.ts} | 15 ++++- libs/components/src/button/button.module.ts | 6 +- libs/components/src/button/button.stories.ts | 55 ++++++++++++++---- libs/components/src/button/index.ts | 2 +- libs/components/src/index.ts | 1 - libs/components/src/submit-button/index.ts | 1 - .../submit-button.component.html | 16 ------ .../submit-button/submit-button.component.ts | 19 ------- .../src/submit-button/submit-button.module.ts | 13 ----- .../submit-button/submit-button.stories.ts | 45 --------------- 26 files changed, 180 insertions(+), 147 deletions(-) create mode 100644 libs/components/src/button/button.component.html rename libs/components/src/button/{button.directive.spec.ts => button.component.spec.ts} (65%) rename libs/components/src/button/{button.directive.ts => button.component.ts} (82%) delete mode 100644 libs/components/src/submit-button/index.ts delete mode 100644 libs/components/src/submit-button/submit-button.component.html delete mode 100644 libs/components/src/submit-button/submit-button.component.ts delete mode 100644 libs/components/src/submit-button/submit-button.module.ts delete mode 100644 libs/components/src/submit-button/submit-button.stories.ts diff --git a/apps/web/src/app/accounts/register-form/register-form.component.html b/apps/web/src/app/accounts/register-form/register-form.component.html index 56364086ff9..d9456d5a6e9 100644 --- a/apps/web/src/app/accounts/register-form/register-form.component.html +++ b/apps/web/src/app/accounts/register-form/register-form.component.html @@ -114,7 +114,9 @@
- {{ "createAccount" | i18n }} + - {{ "logIn" | i18n }} +
diff --git a/apps/web/src/app/accounts/trial-initiation/billing.component.html b/apps/web/src/app/accounts/trial-initiation/billing.component.html index 4486d0672dd..0eb203f72ca 100644 --- a/apps/web/src/app/accounts/trial-initiation/billing.component.html +++ b/apps/web/src/app/accounts/trial-initiation/billing.component.html @@ -40,7 +40,9 @@
- {{ "startTrial" | i18n }} +
diff --git a/apps/web/src/app/organizations/settings/account.component.html b/apps/web/src/app/organizations/settings/account.component.html index 73a2f7872e7..1f2e145ae7a 100644 --- a/apps/web/src/app/organizations/settings/account.component.html +++ b/apps/web/src/app/organizations/settings/account.component.html @@ -66,9 +66,9 @@
- +
diff --git a/apps/web/src/app/organizations/users/enroll-master-password-reset.component.html b/apps/web/src/app/organizations/users/enroll-master-password-reset.component.html index 72f1bbdbda7..4d24e66764e 100644 --- a/apps/web/src/app/organizations/users/enroll-master-password-reset.component.html +++ b/apps/web/src/app/organizations/users/enroll-master-password-reset.component.html @@ -32,9 +32,9 @@
- +

{{ "reportError" | i18n }}...

diff --git a/apps/web/src/app/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/reports/pages/exposed-passwords-report.component.html index 2c5547efc7c..0fce002310b 100644 --- a/apps/web/src/app/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/reports/pages/exposed-passwords-report.component.html @@ -2,9 +2,9 @@

{{ "exposedPasswordsReport" | i18n }}

{{ "exposedPasswordsReportDesc" | i18n }}

- +
{{ "noExposedPasswords" | i18n }} diff --git a/apps/web/src/app/settings/change-kdf.component.html b/apps/web/src/app/settings/change-kdf.component.html index b06cf01d060..1b3b62a03fb 100644 --- a/apps/web/src/app/settings/change-kdf.component.html +++ b/apps/web/src/app/settings/change-kdf.component.html @@ -71,7 +71,7 @@
- + diff --git a/apps/web/src/app/settings/change-password.component.html b/apps/web/src/app/settings/change-password.component.html index 6f36236a7ae..e74881db025 100644 --- a/apps/web/src/app/settings/change-password.component.html +++ b/apps/web/src/app/settings/change-password.component.html @@ -100,7 +100,7 @@ - + diff --git a/apps/web/src/app/settings/emergency-access-add-edit.component.html b/apps/web/src/app/settings/emergency-access-add-edit.component.html index 410489321a1..b438cee937c 100644 --- a/apps/web/src/app/settings/emergency-access-add-edit.component.html +++ b/apps/web/src/app/settings/emergency-access-add-edit.component.html @@ -100,9 +100,15 @@
- {{ - "submit" | i18n - }} + diff --git a/apps/web/src/app/settings/premium.component.html b/apps/web/src/app/settings/premium.component.html index 63995b457ac..13266fd9ca5 100644 --- a/apps/web/src/app/settings/premium.component.html +++ b/apps/web/src/app/settings/premium.component.html @@ -68,9 +68,9 @@ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}
- +
@@ -118,7 +118,7 @@

{{ "paymentChargedAnnually" | i18n }} - + diff --git a/apps/web/src/app/settings/two-factor-setup.component.html b/apps/web/src/app/settings/two-factor-setup.component.html index 698e7fd9c07..b846fdb9816 100644 --- a/apps/web/src/app/settings/two-factor-setup.component.html +++ b/apps/web/src/app/settings/two-factor-setup.component.html @@ -77,9 +77,15 @@ {{ "deviceVerificationDesc" | i18n }} - + diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index 342948bb515..c0a076e545e 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -12,7 +12,6 @@ import { ButtonModule, CalloutModule, FormFieldModule, - SubmitButtonModule, MenuModule, IconModule, } from "@bitwarden/components"; @@ -44,7 +43,6 @@ import "./locales"; ButtonModule, MenuModule, FormFieldModule, - SubmitButtonModule, IconModule, ], exports: [ @@ -63,7 +61,6 @@ import "./locales"; ButtonModule, MenuModule, FormFieldModule, - SubmitButtonModule, IconModule, ], providers: [DatePipe], diff --git a/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.html b/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.html index 54fe44e073c..3d27448753f 100644 --- a/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.html +++ b/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.html @@ -81,7 +81,13 @@ {{ "scimApiKeyHelperText" | i18n }} - + diff --git a/libs/components/src/button/button.component.html b/libs/components/src/button/button.component.html new file mode 100644 index 00000000000..4875c159e92 --- /dev/null +++ b/libs/components/src/button/button.component.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/libs/components/src/button/button.directive.spec.ts b/libs/components/src/button/button.component.spec.ts similarity index 65% rename from libs/components/src/button/button.directive.spec.ts rename to libs/components/src/button/button.component.spec.ts index a7c3024e480..48aa8928e90 100644 --- a/libs/components/src/button/button.directive.spec.ts +++ b/libs/components/src/button/button.component.spec.ts @@ -8,6 +8,7 @@ describe("Button", () => { let fixture: ComponentFixture; let testAppComponent: TestApp; let buttonDebugElement: DebugElement; + let disabledButtonDebugElement: DebugElement; let linkDebugElement: DebugElement; beforeEach(waitForAsync(() => { @@ -20,6 +21,7 @@ describe("Button", () => { fixture = TestBed.createComponent(TestApp); testAppComponent = fixture.debugElement.componentInstance; buttonDebugElement = fixture.debugElement.query(By.css("button")); + disabledButtonDebugElement = fixture.debugElement.query(By.css("button#disabled")); linkDebugElement = fixture.debugElement.query(By.css("a")); })); @@ -60,16 +62,67 @@ describe("Button", () => { expect(buttonDebugElement.nativeElement.classList.contains("tw-block")).toBe(false); expect(linkDebugElement.nativeElement.classList.contains("tw-block")).toBe(false); }); + + it("should not be disabled when loading and disabled are false", () => { + testAppComponent.loading = false; + testAppComponent.disabled = false; + fixture.detectChanges(); + + expect(buttonDebugElement.attributes["loading"]).toBeFalsy(); + expect(linkDebugElement.attributes["loading"]).toBeFalsy(); + expect(buttonDebugElement.nativeElement.disabled).toBeFalsy(); + }); + + it("should be disabled when disabled is true", () => { + testAppComponent.disabled = true; + fixture.detectChanges(); + + expect(buttonDebugElement.nativeElement.disabled).toBeTruthy(); + // Anchor tags cannot be disabled. + }); + + it("should be disabled when attribute disabled is true", () => { + expect(disabledButtonDebugElement.nativeElement.disabled).toBeTruthy(); + }); + + it("should be disabled when loading is true", () => { + testAppComponent.loading = true; + fixture.detectChanges(); + + expect(buttonDebugElement.nativeElement.disabled).toBeTruthy(); + }); }); @Component({ selector: "test-app", template: ` - - Link + + + Link + + + `, }) class TestApp { buttonType: string; block: boolean; + disabled: boolean; + loading: boolean; } diff --git a/libs/components/src/button/button.directive.ts b/libs/components/src/button/button.component.ts similarity index 82% rename from libs/components/src/button/button.directive.ts rename to libs/components/src/button/button.component.ts index 5c4b0039aef..eeba83b8156 100644 --- a/libs/components/src/button/button.directive.ts +++ b/libs/components/src/button/button.component.ts @@ -1,4 +1,4 @@ -import { Input, HostBinding, Directive } from "@angular/core"; +import { Input, HostBinding, Component } from "@angular/core"; export type ButtonTypes = "primary" | "secondary" | "danger"; @@ -38,10 +38,11 @@ const buttonStyles: Record = { ], }; -@Directive({ +@Component({ selector: "button[bitButton], a[bitButton]", + templateUrl: "button.component.html", }) -export class ButtonDirective { +export class ButtonComponent { @HostBinding("class") get classList() { return [ "tw-font-semibold", @@ -65,6 +66,14 @@ export class ButtonDirective { .concat(buttonStyles[this.buttonType ?? "secondary"]); } + @HostBinding("attr.disabled") + get disabledAttr() { + const disabled = this.disabled != null && this.disabled !== false; + return disabled || this.loading ? true : null; + } + @Input() buttonType: ButtonTypes = null; @Input() block?: boolean; + @Input() loading = false; + @Input() disabled = false; } diff --git a/libs/components/src/button/button.module.ts b/libs/components/src/button/button.module.ts index c9c3822abfa..448e7c9dcf6 100644 --- a/libs/components/src/button/button.module.ts +++ b/libs/components/src/button/button.module.ts @@ -1,11 +1,11 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { ButtonDirective } from "./button.directive"; +import { ButtonComponent } from "./button.component"; @NgModule({ imports: [CommonModule], - exports: [ButtonDirective], - declarations: [ButtonDirective], + exports: [ButtonComponent], + declarations: [ButtonComponent], }) export class ButtonModule {} diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index f09b8701b1b..4b9b88d48b1 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -1,12 +1,14 @@ import { Meta, Story } from "@storybook/angular"; -import { ButtonDirective } from "./button.directive"; +import { ButtonComponent } from "./button.component"; export default { title: "Component Library/Button", - component: ButtonDirective, + component: ButtonComponent, args: { buttonType: "primary", + disabled: false, + loading: false, }, parameters: { design: { @@ -16,11 +18,11 @@ export default { }, } as Meta; -const Template: Story = (args: ButtonDirective) => ({ +const Template: Story = (args: ButtonComponent) => ({ props: args, template: ` - - Link + + Link `, }); @@ -39,21 +41,50 @@ Danger.args = { buttonType: "danger", }; -const DisabledTemplate: Story = (args) => ({ +const AllStylesTemplate: Story = (args) => ({ props: args, template: ` - - - + + + `, }); -export const Disabled = DisabledTemplate.bind({}); +export const Loading = AllStylesTemplate.bind({}); +Loading.args = { + disabled: false, + loading: true, +}; + +export const Disabled = AllStylesTemplate.bind({}); Disabled.args = { - size: "small", + disabled: true, + loading: false, +}; + +const DisabledWithAttributeTemplate: Story = (args) => ({ + props: args, + template: ` + + + + + + + + + + + `, +}); + +export const DisabledWithAttribute = DisabledWithAttributeTemplate.bind({}); +DisabledWithAttribute.args = { + disabled: true, + loading: false, }; -const BlockTemplate: Story = (args: ButtonDirective) => ({ +const BlockTemplate: Story = (args: ButtonComponent) => ({ props: args, template: ` diff --git a/libs/components/src/button/index.ts b/libs/components/src/button/index.ts index 1bdd62ddbcf..ff86120cb11 100644 --- a/libs/components/src/button/index.ts +++ b/libs/components/src/button/index.ts @@ -1,2 +1,2 @@ -export * from "./button.directive"; +export * from "./button.component"; export * from "./button.module"; diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 1f1ae6a8d2b..264c655d80f 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -7,7 +7,6 @@ export * from "./icon"; export * from "./icon-button"; export * from "./menu"; export * from "./dialog"; -export * from "./submit-button"; export * from "./link"; export * from "./tabs"; export * from "./toggle-group"; diff --git a/libs/components/src/submit-button/index.ts b/libs/components/src/submit-button/index.ts deleted file mode 100644 index ae7d96d2c1a..00000000000 --- a/libs/components/src/submit-button/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./submit-button.module"; diff --git a/libs/components/src/submit-button/submit-button.component.html b/libs/components/src/submit-button/submit-button.component.html deleted file mode 100644 index 9d9657ba7ee..00000000000 --- a/libs/components/src/submit-button/submit-button.component.html +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/libs/components/src/submit-button/submit-button.component.ts b/libs/components/src/submit-button/submit-button.component.ts deleted file mode 100644 index 27408349da7..00000000000 --- a/libs/components/src/submit-button/submit-button.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Component, HostBinding, Input } from "@angular/core"; - -import { ButtonTypes } from "../button"; - -@Component({ - selector: "bit-submit-button", - templateUrl: "./submit-button.component.html", -}) -export class SubmitButtonComponent { - @Input() buttonType: ButtonTypes = "primary"; - @Input() disabled = false; - @Input() loading: boolean; - - @Input() block?: boolean; - - @HostBinding("class") get classList() { - return this.block == null || this.block === false ? [] : ["tw-w-full", "tw-block"]; - } -} diff --git a/libs/components/src/submit-button/submit-button.module.ts b/libs/components/src/submit-button/submit-button.module.ts deleted file mode 100644 index c7ab7567e64..00000000000 --- a/libs/components/src/submit-button/submit-button.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; - -import { ButtonModule } from "../button"; - -import { SubmitButtonComponent } from "./submit-button.component"; - -@NgModule({ - imports: [CommonModule, ButtonModule], - exports: [SubmitButtonComponent], - declarations: [SubmitButtonComponent], -}) -export class SubmitButtonModule {} diff --git a/libs/components/src/submit-button/submit-button.stories.ts b/libs/components/src/submit-button/submit-button.stories.ts deleted file mode 100644 index cf19b1c8e4b..00000000000 --- a/libs/components/src/submit-button/submit-button.stories.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Meta, moduleMetadata, Story } from "@storybook/angular"; - -import { SubmitButtonComponent } from "./submit-button.component"; -import { SubmitButtonModule } from "./submit-button.module"; - -export default { - title: "Component Library/Submit Button", - component: SubmitButtonComponent, - decorators: [ - moduleMetadata({ - imports: [SubmitButtonModule], - }), - ], - args: { - buttonType: "primary", - loading: false, - block: false, - }, - parameters: { - design: { - type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16733", - }, - }, -} as Meta; - -const Template: Story = (args: SubmitButtonComponent) => ({ - props: args, - template: ` - Submit - `, -}); - -export const Primary = Template.bind({}); -Primary.args = {}; - -export const Loading = Template.bind({}); -Loading.args = { - loading: true, -}; - -export const Disabled = Template.bind({}); -Disabled.args = { - disabled: true, -}; From df5f1883b1ad5347f9bd9ce624da6d053b94ad6b Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 29 Sep 2022 15:55:13 +0200 Subject: [PATCH 08/20] fix non-relative import from src breaking storybook --- apps/web/src/app/accounts/login/login.component.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/web/src/app/accounts/login/login.component.ts b/apps/web/src/app/accounts/login/login.component.ts index 8664ae4a572..2568d5a3e74 100644 --- a/apps/web/src/app/accounts/login/login.component.ts +++ b/apps/web/src/app/accounts/login/login.component.ts @@ -22,8 +22,7 @@ import { Policy } from "@bitwarden/common/models/domain/policy"; import { ListResponse } from "@bitwarden/common/models/response/listResponse"; import { PolicyResponse } from "@bitwarden/common/models/response/policyResponse"; -import { flagEnabled } from "src/utils/flags"; - +import { flagEnabled } from "../../../utils/flags"; import { RouterService, StateService } from "../../core"; @Component({ From 021ec89739a48e9e5ef70f4b604807a09d58e76d Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Thu, 29 Sep 2022 14:10:35 -0400 Subject: [PATCH 09/20] Rename anonymousHub route to anonymous-hub (#3650) --- libs/common/src/services/anonymousHub.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/services/anonymousHub.service.ts b/libs/common/src/services/anonymousHub.service.ts index 13b5898b18b..d0a069414a2 100644 --- a/libs/common/src/services/anonymousHub.service.ts +++ b/libs/common/src/services/anonymousHub.service.ts @@ -32,7 +32,7 @@ export class AnonymousHubService implements AnonymousHubServiceAbstraction { this.url = this.environmentService.getNotificationsUrl(); this.anonHubConnection = new HubConnectionBuilder() - .withUrl(this.url + "/anonymousHub?Token=" + token, { + .withUrl(this.url + "/anonymous-hub?Token=" + token, { skipNegotiation: true, transport: HttpTransportType.WebSockets, }) From 7bdaa8ecca8fb417a9f2106638c852d0eb3dbb90 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 30 Sep 2022 08:00:00 -0700 Subject: [PATCH 10/20] Bumped desktop version to 2022.9.2 (#3541) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> (cherry picked from commit 24c6f5d313f275cc5f49b53906e606d38dff476c) --- apps/desktop/package.json | 2 +- apps/desktop/src/package-lock.json | 4 ++-- apps/desktop/src/package.json | 2 +- package-lock.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 61a3e05b58c..1e67787343c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2022.9.1", + "version": "2022.9.2", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 69a5370b6e5..2e0090096bb 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2022.9.1", + "version": "2022.9.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2022.9.1", + "version": "2022.9.2", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native" diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 705c6dc9be6..0bf1bfd925c 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2022.9.1", + "version": "2022.9.2", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/package-lock.json b/package-lock.json index a0f5a037f80..059f49ffcc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -219,7 +219,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2022.9.1", + "version": "2022.9.2", "hasInstallScript": true, "license": "GPL-3.0" }, From 12dcf28146532f7a23da7b7a74e882f8d9b9c1cf Mon Sep 17 00:00:00 2001 From: Daniel James Smith Date: Tue, 4 Oct 2022 20:43:51 +0200 Subject: [PATCH 11/20] [PS-1514] Do not subscribe to activeAccount-observable and execute load asynchronously (#3608) * Fix async subscribe * Revert "[PS-1066] Browser and Desktop - SSO User does not see Update Master Password screen after Owner does a Admin Password Reset (#3207)" This reverts commit 0eda4185911090313146842038f871c7911640a2. (cherry picked from commit 43d586ff9900cb8bc2ee158ed19cd6823669cc64) --- .../src/popup/accounts/lock.component.ts | 13 ++---------- .../src/app/accounts/lock.component.ts | 11 +--------- libs/angular/src/components/lock.component.ts | 21 ++++++++++++------- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/apps/browser/src/popup/accounts/lock.component.ts b/apps/browser/src/popup/accounts/lock.component.ts index c288a54a65b..775ecaa3ca0 100644 --- a/apps/browser/src/popup/accounts/lock.component.ts +++ b/apps/browser/src/popup/accounts/lock.component.ts @@ -12,7 +12,6 @@ import { LogService } from "@bitwarden/common/abstractions/log.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"; -import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service"; import { AuthenticationStatus } from "@bitwarden/common/enums/authenticationStatus"; @@ -28,8 +27,6 @@ export class LockComponent extends BaseLockComponent { biometricError: string; pendingBiometric = false; - authenicatedUrl = "/tabs/current"; - unAuthenicatedUrl = "/update-temp-password"; constructor( router: Router, @@ -45,8 +42,7 @@ export class LockComponent extends BaseLockComponent { logService: LogService, keyConnectorService: KeyConnectorService, ngZone: NgZone, - private authService: AuthService, - private syncService: SyncService + private authService: AuthService ) { super( router, @@ -63,17 +59,12 @@ export class LockComponent extends BaseLockComponent { keyConnectorService, ngZone ); - + this.successRoute = "/tabs/current"; this.isInitialLockScreen = (window as any).previousPopupUrl == null; } async ngOnInit() { await super.ngOnInit(); - await this.syncService.fullSync(true); - - const forcePasswordReset = await this.stateService.getForcePasswordReset(); - this.successRoute = forcePasswordReset === true ? this.unAuthenicatedUrl : this.authenicatedUrl; - const disableAutoBiometricsPrompt = (await this.stateService.getDisableAutoBiometricsPrompt()) ?? true; diff --git a/apps/desktop/src/app/accounts/lock.component.ts b/apps/desktop/src/app/accounts/lock.component.ts index ff136aa982e..5cb28802796 100644 --- a/apps/desktop/src/app/accounts/lock.component.ts +++ b/apps/desktop/src/app/accounts/lock.component.ts @@ -13,7 +13,6 @@ import { LogService } from "@bitwarden/common/abstractions/log.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"; -import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service"; @@ -25,8 +24,6 @@ const BroadcasterSubscriptionId = "LockComponent"; }) export class LockComponent extends BaseLockComponent { private deferFocus: boolean = null; - authenicatedUrl = "vault"; - unAuthenicatedUrl = "update-temp-password"; constructor( router: Router, @@ -43,8 +40,7 @@ export class LockComponent extends BaseLockComponent { private broadcasterService: BroadcasterService, ngZone: NgZone, logService: LogService, - keyConnectorService: KeyConnectorService, - private syncService: SyncService + keyConnectorService: KeyConnectorService ) { super( router, @@ -67,11 +63,6 @@ export class LockComponent extends BaseLockComponent { await super.ngOnInit(); const autoPromptBiometric = !(await this.stateService.getNoAutoPromptBiometrics()); - await this.syncService.fullSync(true); - - const forcePasswordReset = await this.stateService.getForcePasswordReset(); - this.successRoute = forcePasswordReset === true ? this.unAuthenicatedUrl : this.authenicatedUrl; - // eslint-disable-next-line rxjs-angular/prefer-takeuntil this.route.queryParams.subscribe((params) => { if (this.supportsBiometric && params.promptBiometric && autoPromptBiometric) { diff --git a/libs/angular/src/components/lock.component.ts b/libs/angular/src/components/lock.component.ts index 0932fab7128..a488d874461 100644 --- a/libs/angular/src/components/lock.component.ts +++ b/libs/angular/src/components/lock.component.ts @@ -1,7 +1,7 @@ import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; -import { Subscription } from "rxjs"; -import { take } from "rxjs/operators"; +import { Subject } from "rxjs"; +import { concatMap, take, takeUntil } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; @@ -41,7 +41,7 @@ export class LockComponent implements OnInit, OnDestroy { private invalidPinAttempts = 0; private pinSet: [boolean, boolean]; - private activeAccountSubscription: Subscription; + private destroy$ = new Subject(); constructor( protected router: Router, @@ -60,14 +60,19 @@ export class LockComponent implements OnInit, OnDestroy { ) {} async ngOnInit() { - // eslint-disable-next-line rxjs/no-async-subscribe - this.activeAccountSubscription = this.stateService.activeAccount$.subscribe(async () => { - await this.load(); - }); + this.stateService.activeAccount$ + .pipe( + concatMap(async () => { + await this.load(); + }), + takeUntil(this.destroy$) + ) + .subscribe(); } ngOnDestroy() { - this.activeAccountSubscription.unsubscribe(); + this.destroy$.next(); + this.destroy$.complete(); } async submit() { From 125c3237e5ad6f9cf95b6a5825c39bb44a95ff0a Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 5 Oct 2022 07:41:35 +1000 Subject: [PATCH 12/20] Add organization-options menu to single org (#3678) (cherry picked from commit 7c3255d9fa73c0df714f0323e6e46a08fdfe3646) --- .../organization-filter.component.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/web/src/app/vault/vault-filter/organization-filter/organization-filter.component.html b/apps/web/src/app/vault/vault-filter/organization-filter/organization-filter.component.html index 2977f63ed71..9d5d8b45a7c 100644 --- a/apps/web/src/app/vault/vault-filter/organization-filter/organization-filter.component.html +++ b/apps/web/src/app/vault/vault-filter/organization-filter/organization-filter.component.html @@ -97,6 +97,14 @@ {{ organizations[0].name }} + + + + + + From 5fc9b0a8420049533133d986fa08e8a1a573e0a7 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 5 Oct 2022 09:12:13 -0400 Subject: [PATCH 13/20] Removed passwordless button (#3686) --- apps/web/config/qa.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/config/qa.json b/apps/web/config/qa.json index a0d1b0e88c3..d6b3f2c98ff 100644 --- a/apps/web/config/qa.json +++ b/apps/web/config/qa.json @@ -11,6 +11,6 @@ }, "flags": { "showTrial": true, - "showPasswordless": true + "showPasswordless": false } } From 3638a6617ba05c40a7a4d779a7bfab286d252702 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Thu, 6 Oct 2022 12:31:31 -0400 Subject: [PATCH 14/20] fixes (#3708) --- libs/angular/src/components/export.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/angular/src/components/export.component.ts b/libs/angular/src/components/export.component.ts index 352ee1eed1a..eaa635fc5e5 100644 --- a/libs/angular/src/components/export.component.ts +++ b/libs/angular/src/components/export.component.ts @@ -117,6 +117,7 @@ export class ExportComponent implements OnInit, OnDestroy { await this.userVerificationService.verifyUser(secret); } catch (e) { this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), e.message); + return; } this.doExport(); From cd5c6a810d872ad51d04ba48e9f1f49f209ef92b Mon Sep 17 00:00:00 2001 From: CarleyDiaz-Bitwarden Date: Thu, 6 Oct 2022 18:49:26 -0400 Subject: [PATCH 15/20] encrypted export UI changes --- .../tools/import-export/export.component.html | 107 +++++++----------- apps/web/src/locales/en/messages.json | 2 +- apps/web/src/locales/en_GB/messages.json | 2 +- 3 files changed, 43 insertions(+), 68 deletions(-) diff --git a/apps/web/src/app/tools/import-export/export.component.html b/apps/web/src/app/tools/import-export/export.component.html index 4e27fc70f75..ec758ac5b72 100644 --- a/apps/web/src/app/tools/import-export/export.component.html +++ b/apps/web/src/app/tools/import-export/export.component.html @@ -84,73 +84,48 @@
-
- - {{ "filePassword" | i18n }} - - -
- -
-
-
+ + {{ "filePassword" | i18n }} + + + {{ "exportPasswordDescription" | i18n }} -
-
-
- - {{ "confirmFilePassword" | i18n }} - -
- -
-
-
+ + + + + {{ "confirmFilePassword" | i18n }} + + +
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index efced93ca43..de4bdc2bcb5 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -912,7 +912,7 @@ "message": "This file export will be password protected and require the file password to decrypt." }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "This password will be used to export and import this file." }, "confirmMasterPassword": { "message": "Confirm Master Password" diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index eac57249883..bd9f4616947 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -897,7 +897,7 @@ "message": "This file export will be password protected and require the file password to decrypt." }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "This password will be used to export and import this file." }, "confirmMasterPassword": { "message": "Confirm Master Password" From a09dbe5ff43a1a927120d9b0ec8d9ad0910ca188 Mon Sep 17 00:00:00 2001 From: CarleyDiaz-Bitwarden Date: Fri, 7 Oct 2022 15:09:25 -0400 Subject: [PATCH 16/20] updating ids --- apps/web/src/app/tools/import-export/export.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/tools/import-export/export.component.html b/apps/web/src/app/tools/import-export/export.component.html index ec758ac5b72..c46e5c9fd5b 100644 --- a/apps/web/src/app/tools/import-export/export.component.html +++ b/apps/web/src/app/tools/import-export/export.component.html @@ -87,7 +87,7 @@ {{ "filePassword" | i18n }} {{ "confirmFilePassword" | i18n }} Date: Fri, 7 Oct 2022 16:00:55 -0400 Subject: [PATCH 17/20] Adding aria titles --- apps/web/src/app/tools/import-export/export.component.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/tools/import-export/export.component.html b/apps/web/src/app/tools/import-export/export.component.html index c46e5c9fd5b..64000921e93 100644 --- a/apps/web/src/app/tools/import-export/export.component.html +++ b/apps/web/src/app/tools/import-export/export.component.html @@ -87,11 +87,12 @@ {{ "filePassword" | i18n }}