From 887b2ec78e753fb6a2c0c19a4ecb76b6ef6db263 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 6 Jul 2023 09:16:16 +0200 Subject: [PATCH] [PM-1033] Org invite user creation flow 1 (#5611) * [PM-1033] feat: basic redirection to login initiated * [PM-1033] feat: add ui for TDE enrollment * [PM-1033] feat: implement auto-enroll * [PM-1033] chore: add todo * [PM-1033] feat: add support in browser * [PM-1033] feat: add support for desktop * [PM-1033] feat: improve key check hack to allow regular accounts * [PM-1033] feat: init asymmetric account keys * [PM-1033] chore: temporary fix bug from merge * [PM-1033] feat: properly check if user can go ahead an auto-enroll * [PM-1033] feat: simplify approval required * [PM-1033] feat: rewrite using discrete states * [PM-1033] fix: clean-up and fix merge artifacts * [PM-1033] chore: clean up empty ng-container * [PM-1033] fix: new user identification logic * [PM-1033] feat: optimize data fetching * [PM-1033] feat: split user creating and reset enrollment * [PM-1033] fix: add missing loading false statement * [PM-1033] fix: navigation logic in sso component * [PM-1033] fix: add missing query param * [PM-1033] chore: rename to `ExistingUserUntrustedDevice` * PM-1033 - fix component templates to reference `ExistingUserUntrustedDevice` so clients can build --------- Co-authored-by: Jared Snider --- .../login-decryption-options.component.html | 127 +++++---- .../login-decryption-options.component.ts | 38 ++- .../login-decryption-options.component.html | 47 ++-- .../login-decryption-options.component.ts | 38 ++- .../login-decryption-options.component.html | 100 +++++--- .../login-decryption-options.component.ts | 38 ++- apps/web/src/app/auth/sso.component.ts | 4 +- ...base-login-decryption-options.component.ts | 242 +++++++++++++++--- .../src/auth/components/sso.component.ts | 33 +-- .../platform/abstractions/crypto.service.ts | 11 + .../src/platform/services/crypto.service.ts | 22 ++ .../src/platform/services/state.service.ts | 2 +- 12 files changed, 508 insertions(+), 194 deletions(-) diff --git a/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html b/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html index 25f857ba56..75a3e885b6 100644 --- a/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html +++ b/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html @@ -11,61 +11,94 @@ -
-

{{ "logInInitiated" | i18n }}

-
{{ "deviceApprovalRequired" | i18n }}
-
- -
-
- - -

{{ "uncheckIfPublicDevice" | i18n }}

+ +
+

{{ "logInInitiated" | i18n }}

+
{{ "deviceApprovalRequired" | i18n }}
- -
- -
+ + +
+ + + +
+
+ + +
+

{{ "logInInitiated" | i18n }}

+
+ +
- {{ "requestAdminApproval" | i18n }} - - -
+
+ + +

{{ "uncheckIfPublicDevice" | i18n }}

+
+ + +
+ +
+

-

{{ "loggingInAs" | i18n }} {{ userEmail }}

+

{{ "loggingInAs" | i18n }} {{ data.userEmail }}

{{ "notYou" | i18n }} diff --git a/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.ts b/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.ts index 676ca2da84..3a303e7bf8 100644 --- a/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.ts +++ b/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.ts @@ -1,11 +1,17 @@ import { Component } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Router } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -16,22 +22,36 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid }) export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent { constructor( - formBuilder: FormBuilder, - devicesService: DevicesServiceAbstraction, - stateService: StateService, - router: Router, - messagingService: MessagingService, - loginService: LoginService, - validationService: ValidationService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction + protected formBuilder: FormBuilder, + protected devicesService: DevicesServiceAbstraction, + protected stateService: StateService, + protected router: Router, + protected activatedRoute: ActivatedRoute, + protected messagingService: MessagingService, + protected tokenService: TokenService, + protected loginService: LoginService, + protected organizationApiService: OrganizationApiServiceAbstraction, + protected cryptoService: CryptoService, + protected organizationUserService: OrganizationUserService, + protected apiService: ApiService, + protected i18nService: I18nService, + protected validationService: ValidationService, + protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction ) { super( formBuilder, devicesService, stateService, router, + activatedRoute, messagingService, + tokenService, loginService, + organizationApiService, + cryptoService, + organizationUserService, + apiService, + i18nService, validationService, deviceTrustCryptoService ); diff --git a/apps/desktop/src/auth/login/login-decryption-options/login-decryption-options.component.html b/apps/desktop/src/auth/login/login-decryption-options/login-decryption-options.component.html index b9fd497955..27be404d59 100644 --- a/apps/desktop/src/auth/login/login-decryption-options/login-decryption-options.component.html +++ b/apps/desktop/src/auth/login/login-decryption-options/login-decryption-options.component.html @@ -8,7 +8,13 @@

{{ "logInInitiated" | i18n }}

-
{{ "deviceApprovalRequired" | i18n }}
+
+ {{ "deviceApprovalRequired" | i18n }} +
@@ -25,41 +31,34 @@ {{ "uncheckIfPublicDevice" | i18n }} -
-
-
-
-
-
-
+
+
+ +
+
+
-

{{ "loggingInAs" | i18n }} {{ userEmail }}

+

{{ "loggingInAs" | i18n }} {{ data.userEmail }}

{{ "notYou" | i18n }}
diff --git a/apps/desktop/src/auth/login/login-decryption-options/login-decryption-options.component.ts b/apps/desktop/src/auth/login/login-decryption-options/login-decryption-options.component.ts index f537224ce5..6d2aa11b16 100644 --- a/apps/desktop/src/auth/login/login-decryption-options/login-decryption-options.component.ts +++ b/apps/desktop/src/auth/login/login-decryption-options/login-decryption-options.component.ts @@ -1,11 +1,17 @@ import { Component } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Router } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -16,22 +22,36 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid }) export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent { constructor( - formBuilder: FormBuilder, - devicesService: DevicesServiceAbstraction, - stateService: StateService, - router: Router, - messagingService: MessagingService, - loginService: LoginService, - validationService: ValidationService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction + protected formBuilder: FormBuilder, + protected devicesService: DevicesServiceAbstraction, + protected stateService: StateService, + protected router: Router, + protected activatedRoute: ActivatedRoute, + protected messagingService: MessagingService, + protected tokenService: TokenService, + protected loginService: LoginService, + protected organizationApiService: OrganizationApiServiceAbstraction, + protected cryptoService: CryptoService, + protected organizationUserService: OrganizationUserService, + protected apiService: ApiService, + protected i18nService: I18nService, + protected validationService: ValidationService, + protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction ) { super( formBuilder, devicesService, stateService, router, + activatedRoute, messagingService, + tokenService, loginService, + organizationApiService, + cryptoService, + organizationUserService, + apiService, + i18nService, validationService, deviceTrustCryptoService ); diff --git a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html index 2a63ad56d7..72105340e0 100644 --- a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html +++ b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html @@ -21,55 +21,83 @@ *ngIf="!loading" class="tw-w-full tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6" > -

{{ "loginInitiated" | i18n }}

+ +

{{ "loginInitiated" | i18n }}

-

{{ "deviceApprovalRequired" | i18n }}

+

+ {{ "deviceApprovalRequired" | i18n }} +

-
- - - {{ "rememberThisDevice" | i18n }} - {{ "uncheckIfPublicDevice" | i18n }} - -
+
+ + + {{ "rememberThisDevice" | i18n }} + {{ "uncheckIfPublicDevice" | i18n }} + +
+ +
+ + + + + +
+
+ + +

{{ "loggedIn" | i18n }}

+ +
+ + + {{ "rememberThisDevice" | i18n }} + {{ "uncheckIfPublicDevice" | i18n }} + +
-
- - - - -
+

-

{{ "loggingInAs" | i18n }} {{ userEmail }}

+

{{ "loggingInAs" | i18n }} {{ data.userEmail }}

{{ "notYou" | i18n }}
diff --git a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts index 33806b576f..2924493773 100644 --- a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts +++ b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts @@ -1,11 +1,17 @@ import { Component } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Router } from "@angular/router"; +import { ActivatedRoute, Router } from "@angular/router"; import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @@ -15,22 +21,36 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid }) export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent { constructor( - formBuilder: FormBuilder, - devicesService: DevicesServiceAbstraction, - stateService: StateService, - router: Router, - messagingService: MessagingService, - loginService: LoginService, - validationService: ValidationService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction + protected formBuilder: FormBuilder, + protected devicesService: DevicesServiceAbstraction, + protected stateService: StateService, + protected router: Router, + protected activatedRoute: ActivatedRoute, + protected messagingService: MessagingService, + protected tokenService: TokenService, + protected loginService: LoginService, + protected organizationApiService: OrganizationApiServiceAbstraction, + protected cryptoService: CryptoService, + protected organizationUserService: OrganizationUserService, + protected apiService: ApiService, + protected i18nService: I18nService, + protected validationService: ValidationService, + protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction ) { super( formBuilder, devicesService, stateService, router, + activatedRoute, messagingService, + tokenService, loginService, + organizationApiService, + cryptoService, + organizationUserService, + apiService, + i18nService, validationService, deviceTrustCryptoService ); diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index 420557664e..fbd626db7f 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -38,10 +38,10 @@ export class SsoComponent extends BaseSsoComponent { environmentService: EnvironmentService, passwordGenerationService: PasswordGenerationServiceAbstraction, logService: LogService, - configService: ConfigServiceAbstraction, private orgDomainApiService: OrgDomainApiServiceAbstraction, private loginService: LoginService, - private validationService: ValidationService + private validationService: ValidationService, + configService: ConfigServiceAbstraction ) { super( authService, diff --git a/libs/angular/src/auth/components/base-login-decryption-options.component.ts b/libs/angular/src/auth/components/base-login-decryption-options.component.ts index d3d86682b8..eebd18caae 100644 --- a/libs/angular/src/auth/components/base-login-decryption-options.component.ts +++ b/libs/angular/src/auth/components/base-login-decryption-options.component.ts @@ -1,29 +1,72 @@ import { Directive, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl } from "@angular/forms"; -import { Router } from "@angular/router"; -import { Observable, Subject, catchError, forkJoin, from, of, finalize, takeUntil } from "rxjs"; +import { ActivatedRoute, Router } from "@angular/router"; +import { + firstValueFrom, + map, + switchMap, + Subject, + catchError, + forkJoin, + from, + of, + finalize, + takeUntil, +} from "rxjs"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service"; +import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/abstractions/organization-user/requests"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; +import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { DesktopDeviceTypes, DeviceType, MobileDeviceTypes, } from "@bitwarden/common/enums/device-type.enum"; +import { KeysRequest } from "@bitwarden/common/models/request/keys.request"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { UserKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + +enum State { + NewUser, + ExistingUserUntrustedDevice, +} + +type NewUserData = { + readonly state: State.NewUser; + readonly organizationId: string; + readonly userEmail: string; +}; + +type ExistingUserUntrustedDeviceData = { + readonly state: State.ExistingUserUntrustedDevice; + readonly showApproveFromOtherDeviceBtn: boolean; + readonly showReqAdminApprovalBtn: boolean; + readonly showApproveWithMasterPasswordBtn: boolean; + readonly userEmail: string; +}; + +type Data = NewUserData | ExistingUserUntrustedDeviceData; @Directive() export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); - showApproveFromOtherDeviceBtn: boolean; - showReqAdminApprovalBtn: boolean; - showApproveWithMasterPasswordBtn: boolean; - userEmail: string; + protected State = State; + + protected data?: Data; + protected loading = true; // Remember device means for the user to trust the device rememberDeviceForm = this.formBuilder.group({ @@ -34,43 +77,94 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { return this.rememberDeviceForm?.controls.rememberDevice; } - loading = true; - constructor( protected formBuilder: FormBuilder, protected devicesService: DevicesServiceAbstraction, protected stateService: StateService, protected router: Router, + protected activatedRoute: ActivatedRoute, protected messagingService: MessagingService, + protected tokenService: TokenService, protected loginService: LoginService, + protected organizationApiService: OrganizationApiServiceAbstraction, + protected cryptoService: CryptoService, + protected organizationUserService: OrganizationUserService, + protected apiService: ApiService, + protected i18nService: I18nService, protected validationService: ValidationService, protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction ) {} - ngOnInit() { - // Note: this is probably not a comprehensive write up of all scenarios: + async ngOnInit() { + this.loading = true; - // If the TDE feature flag is enabled and TDE is configured for the org that the user is a member of, - // then new and existing users can be redirected here after completing the SSO flow (and 2FA if enabled). + try { + const accountDecryptionOptions: AccountDecryptionOptions = + await this.stateService.getAccountDecryptionOptions(); - // First we must determine user type (new or existing): + if ( + !accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval && + !accountDecryptionOptions?.hasMasterPassword + ) { + // We are dealing with a new account if: + // - User does not have admin approval (i.e. has not enrolled into admin reset) + // - AND does not have a master password + this.loadNewUserData(); + } else { + this.loadUntrustedDeviceData(accountDecryptionOptions); + } - // New User - // - present user with option to remember the device or not (trust the device) - // - present a continue button to proceed to the vault - // - loadNewUserData() --> will need to load enrollment status and user email address. + // Note: this is probably not a comprehensive write up of all scenarios: - // Existing User - // - Determine if user is an admin with access to account recovery in admin console - // - Determine if user has a MP or not, if not, they must be redirected to set one (see PM-1035) - // - Determine if device is trusted or not via device crypto service (method not yet written) - // - If not trusted, present user with login decryption options (approve from other device, approve with master password, request admin approval) - // - loadUntrustedDeviceData() + // If the TDE feature flag is enabled and TDE is configured for the org that the user is a member of, + // then new and existing users can be redirected here after completing the SSO flow (and 2FA if enabled). - this.loadUntrustedDeviceData(); + // First we must determine user type (new or existing): + + // New User + // - present user with option to remember the device or not (trust the device) + // - present a continue button to proceed to the vault + // - loadNewUserData() --> will need to load enrollment status and user email address. + + // Existing User + // - Determine if user is an admin with access to account recovery in admin console + // - Determine if user has a MP or not, if not, they must be redirected to set one (see PM-1035) + // - Determine if device is trusted or not via device crypto service (method not yet written) + // - If not trusted, present user with login decryption options (approve from other device, approve with master password, request admin approval) + // - loadUntrustedDeviceData() + } catch (err) { + this.validationService.showError(err); + } } - loadUntrustedDeviceData() { + async loadNewUserData() { + const autoEnrollStatus$ = this.activatedRoute.queryParamMap.pipe( + map((params) => params.get("identifier")), + switchMap((identifier) => { + if (identifier == null) { + return of(null); + } + + return from(this.organizationApiService.getAutoEnrollStatus(identifier)); + }) + ); + + const email$ = from(this.stateService.getEmail()).pipe( + catchError((err: unknown) => { + this.validationService.showError(err); + return of(undefined); + }), + takeUntil(this.destroy$) + ); + + const autoEnrollStatus = await firstValueFrom(autoEnrollStatus$); + const email = await firstValueFrom(email$); + + this.data = { state: State.NewUser, organizationId: autoEnrollStatus.id, userEmail: email }; + this.loading = false; + } + + loadUntrustedDeviceData(accountDecryptionOptions: AccountDecryptionOptions) { this.loading = true; const mobileAndDesktopDeviceTypes: DeviceType[] = Array.from(MobileDeviceTypes).concat( @@ -90,16 +184,6 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { takeUntil(this.destroy$) ); - const accountDecryptionOptions$: Observable = from( - this.stateService.getAccountDecryptionOptions() - ).pipe( - catchError((err: unknown) => { - this.validationService.showError(err); - return of(undefined); - }), - takeUntil(this.destroy$) - ); - const email$ = from(this.stateService.getEmail()).pipe( catchError((err: unknown) => { this.validationService.showError(err); @@ -110,7 +194,6 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { forkJoin({ mobileOrDesktopDevicesExistence: mobileOrDesktopDevicesExistence$, - accountDecryptionOptions: accountDecryptionOptions$, email: email$, }) .pipe( @@ -119,16 +202,24 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { this.loading = false; }) ) - .subscribe(({ mobileOrDesktopDevicesExistence, accountDecryptionOptions, email }) => { - this.showApproveFromOtherDeviceBtn = mobileOrDesktopDevicesExistence || false; + .subscribe(({ mobileOrDesktopDevicesExistence, email }) => { + const showApproveFromOtherDeviceBtn = mobileOrDesktopDevicesExistence || false; - this.showReqAdminApprovalBtn = + const showReqAdminApprovalBtn = !!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false; - this.showApproveWithMasterPasswordBtn = + const showApproveWithMasterPasswordBtn = accountDecryptionOptions?.hasMasterPassword || false; - this.userEmail = email; + const userEmail = email; + + this.data = { + state: State.ExistingUserUntrustedDevice, + showApproveFromOtherDeviceBtn, + showReqAdminApprovalBtn, + showApproveWithMasterPasswordBtn, + userEmail, + }; }); } @@ -136,7 +227,11 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { // TODO: plan is to re-use existing login-with-device component but rework it to have two flows // (1) Standard flow for unauthN user based on AuthService status // (2) New flow for authN user based on AuthService status b/c they have just authenticated w/ SSO - this.loginService.setEmail(this.userEmail); + if (this.data.state !== State.ExistingUserUntrustedDevice) { + return; + } + + this.loginService.setEmail(this.data.userEmail); this.router.navigate(["/login-with-device"]); } @@ -162,6 +257,69 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { this.router.navigate(["/lock"]); } + createUser = async () => { + if (this.data.state !== State.NewUser) { + return; + } + + // this.loading to support clients without async-actions-support + this.loading = true; + try { + const { userKey, publicKey, privateKey } = await this.cryptoService.initAccount(); + const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString); + await this.apiService.postAccountKeys(keysRequest); + + await this.passwordResetEnroll(userKey, publicKey, privateKey); + + if (this.rememberDeviceForm.value.rememberDevice) { + await this.deviceTrustCryptoService.trustDevice(); + } + } catch (error) { + this.validationService.showError(error); + } finally { + this.loading = false; + } + }; + + passwordResetEnroll = async (userKey: UserKey, publicKey: string, privateKey: EncString) => { + if (this.data.state !== State.NewUser) { + return; + } + + // this.loading to support clients without async-actions-support + this.loading = true; + try { + const orgKeyResponse = await this.organizationApiService.getKeys(this.data.organizationId); + if (orgKeyResponse == null) { + throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); + } + + const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey); + + // RSA Encrypt user's userKey.key with organization public key + const userId = await this.stateService.getUserId(); + const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey.buffer); + + const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); + resetRequest.resetPasswordKey = encryptedKey.encryptedString; + + await this.organizationUserService.putOrganizationUserResetPasswordEnrollment( + this.data.organizationId, + userId, + resetRequest + ); + + // TODO: On browser this should close the window. But since we might extract + // this logic into a service I'm gonna leaves this as-is untill that + // refactor is done + await this.router.navigate(["/vault"]); + } catch (error) { + this.validationService.showError(error); + } finally { + this.loading = false; + } + }; + logOut() { this.loading = true; // to avoid an awkward delay in browser extension this.messagingService.send("logout"); diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index 382c1d07aa..a565c526c5 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -37,6 +37,7 @@ export class SsoComponent { protected successRoute = "lock"; protected trustedDeviceEncRoute = "login-initiated"; protected changePasswordRoute = "set-password"; + protected tdeLogin = "login-initiated"; protected forcePasswordResetRoute = "update-temp-password"; protected clientId: string; protected redirectUri: string; @@ -190,6 +191,13 @@ export class SsoComponent { this.formPromise = this.authService.logIn(credentials); const response = await this.formPromise; + const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlagBool( + FeatureFlag.TrustedDeviceEncryption + ); + + const accountDecryptionOptions: AccountDecryptionOptions = + await this.stateService.getAccountDecryptionOptions(); + if (response.requiresTwoFactor) { if (this.onSuccessfulLoginTwoFactorNavigate != null) { await this.onSuccessfulLoginTwoFactorNavigate(); @@ -201,6 +209,15 @@ export class SsoComponent { }, }); } + } else if ( + trustedDeviceEncryptionFeatureActive && + accountDecryptionOptions.trustedDeviceOption !== undefined + ) { + this.router.navigate([this.trustedDeviceEncRoute], { + queryParams: { + identifier: orgIdFromState, + }, + }); } else if (response.resetMasterPassword) { // TODO: for TDE, we are going to deprecate using response.resetMasterPassword // and instead rely on accountDecryptionOptions to determine if the user needs to set a password @@ -228,21 +245,7 @@ export class SsoComponent { if (this.onSuccessfulLoginNavigate != null) { await this.onSuccessfulLoginNavigate(); } else { - const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlagBool( - FeatureFlag.TrustedDeviceEncryption - ); - - const accountDecryptionOptions: AccountDecryptionOptions = - await this.stateService.getAccountDecryptionOptions(); - - if ( - trustedDeviceEncryptionFeatureActive && - accountDecryptionOptions.trustedDeviceOption !== undefined - ) { - this.router.navigate([this.trustedDeviceEncRoute]); - } else { - this.router.navigate([this.successRoute]); - } + this.router.navigate([this.successRoute]); } } } catch (e) { diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 39fa802da0..51e1b45749 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -336,6 +336,17 @@ export abstract class CryptoService { rsaDecrypt: (encValue: string, privateKeyValue?: ArrayBuffer) => Promise; randomNumber: (min: number, max: number) => Promise; + /** + * Initialize all necessary crypto keys needed for a new account. + * Warning! This completely replaces any existing keys! + * @returns The user's newly created public key, private key, and encrypted private key + */ + initAccount: () => Promise<{ + userKey: UserKey; + publicKey: string; + privateKey: EncString; + }>; + /** * @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead. */ diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 756f813d8d..b9742b64b0 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -690,6 +690,28 @@ export class CryptoService implements CryptoServiceAbstraction { return true; } + /** + * Initialize all necessary crypto keys needed for a new account. + * Warning! This completely replaces any existing keys! + */ + async initAccount(): Promise<{ + userKey: UserKey; + publicKey: string; + privateKey: EncString; + }> { + const randomBytes = await this.cryptoFunctionService.randomBytes(64); + const userKey = new SymmetricCryptoKey(randomBytes) as UserKey; + const [publicKey, privateKey] = await this.makeKeyPair(userKey); + await this.stateService.setUserKey(userKey); + await this.stateService.setEncryptedPrivateKey(privateKey.encryptedString); + + return { + userKey, + publicKey, + privateKey, + }; + } + /** * Regenerates any additional keys if needed. Useful to make sure * other keys stay in sync when the user key has been rotated. diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 98a140f59d..cdd041e37c 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -582,7 +582,7 @@ export class StateService< this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); - if (options.userId == this.activeAccountSubject.getValue()) { + if (options?.userId == this.activeAccountSubject.getValue()) { const nextValue = value != null; // Avoid emitting if we are already unlocked