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:
parent
e50e524920
commit
887b2ec78e
@ -11,61 +11,94 @@
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="!loading">
|
||||
<div class="standard-x-margin">
|
||||
<p class="lead">{{ "logInInitiated" | i18n }}</p>
|
||||
<h6 class="mb-20px">{{ "deviceApprovalRequired" | i18n }}</h6>
|
||||
</div>
|
||||
|
||||
<form
|
||||
id="rememberDeviceForm"
|
||||
class="mb-20px standard-x-margin"
|
||||
[formGroup]="rememberDeviceForm"
|
||||
>
|
||||
<div class="">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="rememberDevice"
|
||||
name="rememberDevice"
|
||||
formControlName="rememberDevice"
|
||||
/>
|
||||
<label for="rememberDevice">
|
||||
{{ "rememberThisDevice" | i18n }}
|
||||
</label>
|
||||
<p id="rememberThisDeviceHintText">{{ "uncheckIfPublicDevice" | i18n }}</p>
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="box mb-20px">
|
||||
<button
|
||||
*ngIf="showApproveFromOtherDeviceBtn"
|
||||
(click)="approveFromOtherDevice()"
|
||||
type="button"
|
||||
class="btn primary block"
|
||||
<form
|
||||
id="rememberDeviceForm"
|
||||
class="mb-20px standard-x-margin"
|
||||
[formGroup]="rememberDeviceForm"
|
||||
>
|
||||
<b>{{ "approveFromYourOtherDevice" | i18n }}</b>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="showReqAdminApprovalBtn"
|
||||
(click)="requestAdminApproval()"
|
||||
type="button"
|
||||
class="btn block btn-top-margin"
|
||||
<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
|
||||
*ngIf="data.showApproveFromOtherDeviceBtn"
|
||||
(click)="approveFromOtherDevice()"
|
||||
type="button"
|
||||
class="btn primary block"
|
||||
>
|
||||
<b>{{ "approveFromYourOtherDevice" | i18n }}</b>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="data.showReqAdminApprovalBtn"
|
||||
(click)="requestAdminApproval()"
|
||||
type="button"
|
||||
class="btn block btn-top-margin"
|
||||
>
|
||||
{{ "requestAdminApproval" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="data.showApproveWithMasterPasswordBtn"
|
||||
type="button"
|
||||
class="btn block btn-top-margin"
|
||||
(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"
|
||||
>
|
||||
{{ "requestAdminApproval" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn block btn-top-margin"
|
||||
*ngIf="showApproveWithMasterPasswordBtn"
|
||||
(click)="approveWithMasterPassword()"
|
||||
>
|
||||
{{ "approveWithMasterPassword" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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"
|
||||
>
|
||||
<h2 bitTypography="h2" class="tw-mb-6">{{ "loginInitiated" | i18n }}</h2>
|
||||
<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>
|
||||
<input type="checkbox" bitCheckbox formControlName="rememberDevice" />
|
||||
<bit-label>{{ "rememberThisDevice" | i18n }} </bit-label>
|
||||
<bit-hint bitTypography="body2">{{ "uncheckIfPublicDevice" | i18n }}</bit-hint>
|
||||
</bit-form-control>
|
||||
</form>
|
||||
<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>
|
||||
|
||||
<div class="tw-mb-6 tw-flex tw-flex-col tw-space-y-3">
|
||||
<button
|
||||
*ngIf="data.showApproveFromOtherDeviceBtn"
|
||||
(click)="approveFromOtherDevice()"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
block
|
||||
>
|
||||
{{ "approveFromYourOtherDevice" | i18n }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="data.showReqAdminApprovalBtn"
|
||||
(click)="requestAdminApproval()"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
>
|
||||
{{ "requestAdminApproval" | i18n }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
*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>
|
||||
|
||||
<div class="tw-mb-6 tw-flex tw-flex-col tw-space-y-3">
|
||||
<button
|
||||
*ngIf="showApproveFromOtherDeviceBtn"
|
||||
(click)="approveFromOtherDevice()"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="primary"
|
||||
[block]="true"
|
||||
block
|
||||
class="tw-mb-6"
|
||||
[bitAction]="createUser"
|
||||
>
|
||||
{{ "approveFromYourOtherDevice" | i18n }}
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="showReqAdminApprovalBtn"
|
||||
(click)="requestAdminApproval()"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
>
|
||||
{{ "requestAdminApproval" | i18n }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="showApproveWithMasterPasswordBtn"
|
||||
(click)="approveWithMasterPassword()"
|
||||
bitButton
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
>
|
||||
{{ "approveWithMasterPassword" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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,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<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");
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user