1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-29 12:55:21 +01:00

[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 <jsnider@bitwarden.com>
This commit is contained in:
Andreas Coroiu 2023-07-06 09:16:16 +02:00 committed by GitHub
parent e50e524920
commit 887b2ec78e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 508 additions and 194 deletions

View File

@ -11,6 +11,7 @@
</div>
<ng-container *ngIf="!loading">
<ng-container *ngIf="data.state == State.ExistingUserUntrustedDevice">
<div class="standard-x-margin">
<p class="lead">{{ "logInInitiated" | i18n }}</p>
<h6 class="mb-20px">{{ "deviceApprovalRequired" | i18n }}</h6>
@ -21,7 +22,7 @@
class="mb-20px standard-x-margin"
[formGroup]="rememberDeviceForm"
>
<div class="">
<div>
<input
type="checkbox"
id="rememberDevice"
@ -37,7 +38,7 @@
<div class="box mb-20px">
<button
*ngIf="showApproveFromOtherDeviceBtn"
*ngIf="data.showApproveFromOtherDeviceBtn"
(click)="approveFromOtherDevice()"
type="button"
class="btn primary block"
@ -45,7 +46,7 @@
<b>{{ "approveFromYourOtherDevice" | i18n }}</b>
</button>
<button
*ngIf="showReqAdminApprovalBtn"
*ngIf="data.showReqAdminApprovalBtn"
(click)="requestAdminApproval()"
type="button"
class="btn block btn-top-margin"
@ -53,19 +54,51 @@
{{ "requestAdminApproval" | i18n }}
</button>
<button
*ngIf="data.showApproveWithMasterPasswordBtn"
type="button"
class="btn block btn-top-margin"
*ngIf="showApproveWithMasterPasswordBtn"
(click)="approveWithMasterPassword()"
>
{{ "approveWithMasterPassword" | i18n }}
</button>
</div>
</ng-container>
<ng-container *ngIf="data.state == State.NewUser">
<div class="standard-x-margin">
<p class="lead">{{ "logInInitiated" | i18n }}</p>
</div>
<form
id="rememberDeviceForm"
class="mb-20px standard-x-margin"
[formGroup]="rememberDeviceForm"
>
<div>
<input
type="checkbox"
id="rememberDevice"
name="rememberDevice"
formControlName="rememberDevice"
/>
<label for="rememberDevice">
{{ "rememberThisDevice" | i18n }}
</label>
<p id="rememberThisDeviceHintText">{{ "uncheckIfPublicDevice" | i18n }}</p>
</div>
</form>
<div class="box mb-20px">
<button (click)="createUser()" type="button" class="btn primary block">
<b>{{ "continue" | i18n }}</b>
</button>
</div>
</ng-container>
<hr class="muted-hr mx-5px mb-20px" />
<div class="small mx-5px">
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ userEmail }}</p>
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ data.userEmail }}</p>
<a tabindex="0" role="button" style="cursor: pointer" (click)="logOut()">{{
"notYou" | i18n
}}</a>

View File

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

View File

@ -8,7 +8,13 @@
<ng-container *ngIf="!loading">
<h1 id="heading">{{ "logInInitiated" | i18n }}</h1>
<h6 id="subHeading" class="standard-bottom-margin">{{ "deviceApprovalRequired" | i18n }}</h6>
<h6
*ngIf="data.state == State.ExistingUserUntrustedDevice"
id="subHeading"
class="standard-bottom-margin"
>
{{ "deviceApprovalRequired" | i18n }}
</h6>
<form id="rememberDeviceForm" class="standard-bottom-margin" [formGroup]="rememberDeviceForm">
<div class="checkbox">
@ -25,41 +31,34 @@
<span id="rememberThisDeviceHintText">{{ "uncheckIfPublicDevice" | i18n }}</span>
</form>
<div class="buttons with-rows">
<div class="buttons-row" *ngIf="showApproveFromOtherDeviceBtn">
<button
(click)="approveFromOtherDevice()"
type="button"
class="btn primary block"
[appA11yTitle]="'approveFromYourOtherDevice' | i18n"
>
<div *ngIf="data.state == State.ExistingUserUntrustedDevice" class="buttons with-rows">
<div class="buttons-row" *ngIf="data.showApproveFromOtherDeviceBtn">
<button (click)="approveFromOtherDevice()" type="button" class="btn primary block">
{{ "approveFromYourOtherDevice" | i18n }}
</button>
</div>
<div class="buttons-row" *ngIf="showReqAdminApprovalBtn">
<button
(click)="requestAdminApproval()"
type="button"
class="btn block"
[appA11yTitle]="'requestAdminApproval' | i18n"
>
<div class="buttons-row" *ngIf="data.showReqAdminApprovalBtn">
<button (click)="requestAdminApproval()" type="button" class="btn block">
{{ "requestAdminApproval" | i18n }}
</button>
</div>
<div class="buttons-row" *ngIf="showApproveWithMasterPasswordBtn">
<button
(click)="approveWithMasterPassword()"
type="button"
class="btn block"
[appA11yTitle]="'approveWithMasterPassword' | i18n"
>
<div class="buttons-row" *ngIf="data.showApproveWithMasterPasswordBtn">
<button (click)="approveWithMasterPassword()" type="button" class="btn block">
{{ "approveWithMasterPassword" | i18n }}
</button>
</div>
</div>
<div *ngIf="data.state == State.NewUser" class="buttons with-rows">
<div class="buttons-row">
<button (click)="createUser()" type="button" class="btn block">
{{ "continue" | i18n }}
</button>
</div>
</div>
<div style="text-align: center">
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ userEmail }}</p>
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ data.userEmail }}</p>
<a [routerLink]="[]" (click)="logOut()">{{ "notYou" | i18n }}</a>
</div>
</ng-container>

View File

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

View File

@ -21,9 +21,12 @@
*ngIf="!loading"
class="tw-w-full tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<ng-container *ngIf="data.state == State.ExistingUserUntrustedDevice">
<h2 bitTypography="h2" class="tw-mb-6">{{ "loginInitiated" | i18n }}</h2>
<p bitTypography="body1" class="tw-mb-6">{{ "deviceApprovalRequired" | i18n }}</p>
<p bitTypography="body1" class="tw-mb-6">
{{ "deviceApprovalRequired" | i18n }}
</p>
<form [formGroup]="rememberDeviceForm">
<bit-form-control>
@ -35,18 +38,18 @@
<div class="tw-mb-6 tw-flex tw-flex-col tw-space-y-3">
<button
*ngIf="showApproveFromOtherDeviceBtn"
*ngIf="data.showApproveFromOtherDeviceBtn"
(click)="approveFromOtherDevice()"
bitButton
type="button"
buttonType="primary"
[block]="true"
block
>
{{ "approveFromYourOtherDevice" | i18n }}
</button>
<button
*ngIf="showReqAdminApprovalBtn"
*ngIf="data.showReqAdminApprovalBtn"
(click)="requestAdminApproval()"
bitButton
type="button"
@ -56,20 +59,45 @@
</button>
<button
*ngIf="showApproveWithMasterPasswordBtn"
*ngIf="data.showApproveWithMasterPasswordBtn"
(click)="approveWithMasterPassword()"
bitButton
type="button"
buttonType="secondary"
block
>
{{ "approveWithMasterPassword" | i18n }}
</button>
</div>
</ng-container>
<ng-container *ngIf="data.state == State.NewUser">
<h2 bitTypography="h2" class="tw-mb-6">{{ "loggedIn" | i18n }}</h2>
<form [formGroup]="rememberDeviceForm">
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="rememberDevice" />
<bit-label>{{ "rememberThisDevice" | i18n }} </bit-label>
<bit-hint bitTypography="body2">{{ "uncheckIfPublicDevice" | i18n }}</bit-hint>
</bit-form-control>
</form>
<button
bitButton
type="button"
buttonType="primary"
block
class="tw-mb-6"
[bitAction]="createUser"
>
{{ "continue" | i18n }}
</button>
</ng-container>
<hr class="tw-mb-6 tw-mt-0" />
<div class="tw-m-0 tw-text-sm">
<p class="tw-mb-1">{{ "loggingInAs" | i18n }} {{ userEmail }}</p>
<p class="tw-mb-1">{{ "loggingInAs" | i18n }} {{ data.userEmail }}</p>
<a [routerLink]="[]" (click)="logOut()">{{ "notYou" | i18n }}</a>
</div>
</div>

View File

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

View File

@ -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,

View File

@ -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<void>();
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,20 +77,43 @@ 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() {
async ngOnInit() {
this.loading = true;
try {
const accountDecryptionOptions: AccountDecryptionOptions =
await this.stateService.getAccountDecryptionOptions();
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);
}
// Note: this is probably not a comprehensive write up of all scenarios:
// If the TDE feature flag is enabled and TDE is configured for the org that the user is a member of,
@ -66,11 +132,39 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy {
// - 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()
this.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<AccountDecryptionOptions> = 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");

View File

@ -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
@ -227,24 +244,10 @@ 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]);
}
}
}
} catch (e) {
this.logService.error(e);

View File

@ -336,6 +336,17 @@ export abstract class CryptoService {
rsaDecrypt: (encValue: string, privateKeyValue?: ArrayBuffer) => Promise<ArrayBuffer>;
randomNumber: (min: number, max: number) => Promise<number>;
/**
* 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.
*/

View File

@ -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.

View File

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