mirror of
https://github.com/bitwarden/browser
synced 2025-12-23 19:53:43 +00:00
Merge branch 'main' of https://github.com/bitwarden/clients into vault/pm-18707/desktop-sync-issues
This commit is contained in:
@@ -39,11 +39,10 @@ export class Collection extends Domain {
|
||||
}
|
||||
|
||||
decrypt(orgKey: OrgKey): Promise<CollectionView> {
|
||||
return this.decryptObj(
|
||||
return this.decryptObj<Collection, CollectionView>(
|
||||
this,
|
||||
new CollectionView(this),
|
||||
{
|
||||
name: null,
|
||||
},
|
||||
["name"],
|
||||
this.organizationId,
|
||||
orgKey,
|
||||
);
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder, FormControl } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import {
|
||||
firstValueFrom,
|
||||
switchMap,
|
||||
Subject,
|
||||
catchError,
|
||||
from,
|
||||
of,
|
||||
finalize,
|
||||
takeUntil,
|
||||
defer,
|
||||
throwError,
|
||||
map,
|
||||
Observable,
|
||||
take,
|
||||
} from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
LoginEmailServiceAbstraction,
|
||||
UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
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 BaseLoginDecryptionOptionsComponentV1 implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
protected State = State;
|
||||
|
||||
protected data?: Data;
|
||||
protected loading = true;
|
||||
|
||||
private email$: Observable<string>;
|
||||
|
||||
activeAccountId: UserId;
|
||||
|
||||
// Remember device means for the user to trust the device
|
||||
rememberDeviceForm = this.formBuilder.group({
|
||||
rememberDevice: [true],
|
||||
});
|
||||
|
||||
get rememberDevice(): FormControl<boolean> {
|
||||
return this.rememberDeviceForm?.controls.rememberDevice;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected formBuilder: FormBuilder,
|
||||
protected devicesService: DevicesServiceAbstraction,
|
||||
protected stateService: StateService,
|
||||
protected router: Router,
|
||||
protected activatedRoute: ActivatedRoute,
|
||||
protected messagingService: MessagingService,
|
||||
protected tokenService: TokenService,
|
||||
protected loginEmailService: LoginEmailServiceAbstraction,
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected keyService: KeyService,
|
||||
protected organizationUserApiService: OrganizationUserApiService,
|
||||
protected apiService: ApiService,
|
||||
protected i18nService: I18nService,
|
||||
protected validationService: ValidationService,
|
||||
protected deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
protected accountService: AccountService,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.loading = true;
|
||||
this.activeAccountId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
this.email$ = this.accountService.activeAccount$.pipe(
|
||||
map((a) => a?.email),
|
||||
catchError((err: unknown) => {
|
||||
this.validationService.showError(err);
|
||||
return of(undefined);
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
|
||||
this.setupRememberDeviceValueChanges();
|
||||
|
||||
// Persist user choice from state if it exists
|
||||
await this.setRememberDeviceDefaultValue();
|
||||
|
||||
try {
|
||||
const userDecryptionOptions = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
|
||||
// see sso-login.strategy - to determine if a user is new or not it just checks if there is a key on the token response..
|
||||
// can we check if they have a user key or master key in crypto service? Would that be sufficient?
|
||||
if (
|
||||
!userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval &&
|
||||
!userDecryptionOptions?.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
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.loadNewUserData();
|
||||
} else {
|
||||
this.loadUntrustedDeviceData(userDecryptionOptions);
|
||||
}
|
||||
|
||||
// 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,
|
||||
// then new and existing users can be redirected here after completing the SSO flow (and 2FA if enabled).
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
private async setRememberDeviceDefaultValue() {
|
||||
const rememberDeviceFromState = await this.deviceTrustService.getShouldTrustDevice(
|
||||
this.activeAccountId,
|
||||
);
|
||||
|
||||
const rememberDevice = rememberDeviceFromState ?? true;
|
||||
|
||||
this.rememberDevice.setValue(rememberDevice);
|
||||
}
|
||||
|
||||
private setupRememberDeviceValueChanges() {
|
||||
this.rememberDevice.valueChanges
|
||||
.pipe(
|
||||
switchMap((value) =>
|
||||
defer(() => this.deviceTrustService.setShouldTrustDevice(this.activeAccountId, value)),
|
||||
),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async loadNewUserData() {
|
||||
const autoEnrollStatus$ = defer(() =>
|
||||
this.ssoLoginService.getActiveUserOrganizationSsoIdentifier(this.activeAccountId),
|
||||
).pipe(
|
||||
switchMap((organizationIdentifier) => {
|
||||
if (organizationIdentifier == undefined) {
|
||||
return throwError(() => new Error(this.i18nService.t("ssoIdentifierRequired")));
|
||||
}
|
||||
|
||||
return from(this.organizationApiService.getAutoEnrollStatus(organizationIdentifier));
|
||||
}),
|
||||
catchError((err: unknown) => {
|
||||
this.validationService.showError(err);
|
||||
return of(undefined);
|
||||
}),
|
||||
);
|
||||
|
||||
const autoEnrollStatus = await firstValueFrom(autoEnrollStatus$);
|
||||
const email = await firstValueFrom(this.email$);
|
||||
|
||||
this.data = { state: State.NewUser, organizationId: autoEnrollStatus.id, userEmail: email };
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
loadUntrustedDeviceData(userDecryptionOptions: UserDecryptionOptions) {
|
||||
this.loading = true;
|
||||
|
||||
this.email$
|
||||
.pipe(
|
||||
take(1),
|
||||
finalize(() => {
|
||||
this.loading = false;
|
||||
}),
|
||||
)
|
||||
.subscribe((email) => {
|
||||
const showApproveFromOtherDeviceBtn =
|
||||
userDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false;
|
||||
|
||||
const showReqAdminApprovalBtn =
|
||||
!!userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false;
|
||||
|
||||
const showApproveWithMasterPasswordBtn = userDecryptionOptions?.hasMasterPassword || false;
|
||||
|
||||
const userEmail = email;
|
||||
|
||||
this.data = {
|
||||
state: State.ExistingUserUntrustedDevice,
|
||||
showApproveFromOtherDeviceBtn,
|
||||
showReqAdminApprovalBtn,
|
||||
showApproveWithMasterPasswordBtn,
|
||||
userEmail,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async approveFromOtherDevice() {
|
||||
if (this.data.state !== State.ExistingUserUntrustedDevice) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loginEmailService.setLoginEmail(this.data.userEmail);
|
||||
await this.router.navigate(["/login-with-device"]);
|
||||
}
|
||||
|
||||
async requestAdminApproval() {
|
||||
this.loginEmailService.setLoginEmail(this.data.userEmail);
|
||||
await this.router.navigate(["/admin-approval-requested"]);
|
||||
}
|
||||
|
||||
async approveWithMasterPassword() {
|
||||
await this.router.navigate(["/lock"], { queryParams: { from: "login-initiated" } });
|
||||
}
|
||||
|
||||
async createUser() {
|
||||
if (this.data.state !== State.NewUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this.loading to support clients without async-actions-support
|
||||
this.loading = true;
|
||||
// errors must be caught in child components to prevent navigation
|
||||
try {
|
||||
const { publicKey, privateKey } = await this.keyService.initAccount();
|
||||
const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString);
|
||||
await this.apiService.postAccountKeys(keysRequest);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("accountSuccessfullyCreated"),
|
||||
});
|
||||
|
||||
await this.passwordResetEnrollmentService.enroll(this.data.organizationId);
|
||||
|
||||
if (this.rememberDeviceForm.value.rememberDevice) {
|
||||
await this.deviceTrustService.trustDevice(this.activeAccountId);
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
logOut() {
|
||||
this.loading = true; // to avoid an awkward delay in browser extension
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Subject, firstValueFrom, map, takeUntil } from "rxjs";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||
import { Component, EventEmitter, Output, Input, OnInit, OnDestroy } from "@angular/core";
|
||||
@@ -7,8 +5,6 @@ import { ActivatedRoute } from "@angular/router";
|
||||
import { Observable, map, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { SelfHostedEnvConfigDialogComponent } from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import {
|
||||
EnvironmentService,
|
||||
Region,
|
||||
@@ -88,7 +84,6 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
protected environmentService: EnvironmentService,
|
||||
private route: ActivatedRoute,
|
||||
private dialogService: DialogService,
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
@@ -113,24 +108,18 @@ export class EnvironmentSelectorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the self-hosted settings dialog.
|
||||
*
|
||||
* If the `UnauthenticatedExtensionUIRefresh` feature flag is enabled,
|
||||
* the self-hosted settings dialog is opened directly. Otherwise, the
|
||||
* `onOpenSelfHostedSettings` event is emitted.
|
||||
* Opens the self-hosted settings dialog when the self-hosted option is selected.
|
||||
*/
|
||||
if (option === Region.SelfHosted) {
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.UnauthenticatedExtensionUIRefresh)) {
|
||||
if (await SelfHostedEnvConfigDialogComponent.open(this.dialogService)) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("environmentSaved"),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.onOpenSelfHostedSettings.emit();
|
||||
}
|
||||
if (
|
||||
option === Region.SelfHosted &&
|
||||
(await SelfHostedEnvConfigDialogComponent.open(this.dialogService))
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("environmentSaved"),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Output } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
|
||||
import {
|
||||
EnvironmentService,
|
||||
Region,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { ModalService } from "../../services/modal.service";
|
||||
|
||||
@Directive()
|
||||
export class EnvironmentComponent {
|
||||
@Output() onSaved = new EventEmitter();
|
||||
|
||||
iconsUrl: string;
|
||||
identityUrl: string;
|
||||
apiUrl: string;
|
||||
webVaultUrl: string;
|
||||
notificationsUrl: string;
|
||||
baseUrl: string;
|
||||
showCustom = false;
|
||||
|
||||
constructor(
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected i18nService: I18nService,
|
||||
private modalService: ModalService,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
this.environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
|
||||
if (env.getRegion() !== Region.SelfHosted) {
|
||||
this.baseUrl = "";
|
||||
this.webVaultUrl = "";
|
||||
this.apiUrl = "";
|
||||
this.identityUrl = "";
|
||||
this.iconsUrl = "";
|
||||
this.notificationsUrl = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const urls = env.getUrls();
|
||||
this.baseUrl = urls.base || "";
|
||||
this.webVaultUrl = urls.webVault || "";
|
||||
this.apiUrl = urls.api || "";
|
||||
this.identityUrl = urls.identity || "";
|
||||
this.iconsUrl = urls.icons || "";
|
||||
this.notificationsUrl = urls.notifications || "";
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
await this.environmentService.setEnvironment(Region.SelfHosted, {
|
||||
base: this.baseUrl,
|
||||
api: this.apiUrl,
|
||||
identity: this.identityUrl,
|
||||
webVault: this.webVaultUrl,
|
||||
icons: this.iconsUrl,
|
||||
notifications: this.notificationsUrl,
|
||||
});
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("environmentSaved"),
|
||||
});
|
||||
this.saved();
|
||||
}
|
||||
|
||||
toggleCustom() {
|
||||
this.showCustom = !this.showCustom;
|
||||
}
|
||||
|
||||
protected saved() {
|
||||
this.onSaved.emit();
|
||||
this.modalService.closeAll();
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PasswordHintRequest } from "@bitwarden/common/auth/models/request/password-hint.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
@Directive()
|
||||
export class HintComponent implements OnInit {
|
||||
email = "";
|
||||
formPromise: Promise<any>;
|
||||
|
||||
protected successRoute = "login";
|
||||
protected onSuccessfulSubmit: () => void;
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
protected i18nService: I18nService,
|
||||
protected apiService: ApiService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
protected toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.email = (await firstValueFrom(this.loginEmailService.loginEmail$)) ?? "";
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.email == null || this.email === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("emailRequired"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (this.email.indexOf("@") === -1) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("invalidEmail"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.apiService.postPasswordHint(new PasswordHintRequest(this.email));
|
||||
await this.formPromise;
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("masterPassSent"),
|
||||
});
|
||||
if (this.onSuccessfulSubmit != null) {
|
||||
this.onSuccessfulSubmit();
|
||||
} else if (this.router != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,401 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, NavigationSkipped, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, of } from "rxjs";
|
||||
import { switchMap, take, takeUntil } from "rxjs/operators";
|
||||
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
PasswordLoginCredentials,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import {
|
||||
AllValidationErrors,
|
||||
FormValidationErrorsService,
|
||||
} from "../../platform/abstractions/form-validation-errors.service";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
|
||||
@Directive()
|
||||
export class LoginComponentV1 extends CaptchaProtectedComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("masterPasswordInput", { static: true }) masterPasswordInput: ElementRef;
|
||||
|
||||
showPassword = false;
|
||||
formPromise: Promise<AuthResult>;
|
||||
|
||||
onSuccessfulLogin: () => Promise<any>;
|
||||
onSuccessfulLoginNavigate: (userId: UserId) => Promise<any>;
|
||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||
|
||||
showLoginWithDevice: boolean;
|
||||
validatedEmail = false;
|
||||
paramEmailSet = false;
|
||||
|
||||
get emailFormControl() {
|
||||
return this.formGroup.controls.email;
|
||||
}
|
||||
|
||||
formGroup = this.formBuilder.nonNullable.group({
|
||||
email: ["", [Validators.required, Validators.email]],
|
||||
masterPassword: [
|
||||
"",
|
||||
[Validators.required, Validators.minLength(Utils.originalMinimumPasswordLength)],
|
||||
],
|
||||
rememberEmail: [false],
|
||||
});
|
||||
|
||||
protected twoFactorRoute = "2fa";
|
||||
protected successRoute = "vault";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
|
||||
protected destroy$ = new Subject<void>();
|
||||
|
||||
get loggedEmail() {
|
||||
return this.formGroup.controls.email.value;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected devicesApiService: DevicesApiServiceAbstraction,
|
||||
protected appIdService: AppIdService,
|
||||
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
protected router: Router,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
protected stateService: StateService,
|
||||
environmentService: EnvironmentService,
|
||||
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
protected logService: LogService,
|
||||
protected ngZone: NgZone,
|
||||
protected formBuilder: FormBuilder,
|
||||
protected formValidationErrorService: FormValidationErrorsService,
|
||||
protected route: ActivatedRoute,
|
||||
protected loginEmailService: LoginEmailServiceAbstraction,
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService, toastService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.route?.queryParams
|
||||
.pipe(
|
||||
switchMap((params) => {
|
||||
if (!params) {
|
||||
// If no params,loadEmailSettings from state
|
||||
return this.loadEmailSettings();
|
||||
}
|
||||
|
||||
const queryParamsEmail = params.email;
|
||||
|
||||
if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) {
|
||||
this.formGroup.controls.email.setValue(queryParamsEmail);
|
||||
this.paramEmailSet = true;
|
||||
}
|
||||
|
||||
// If paramEmailSet is false, loadEmailSettings from state
|
||||
return this.paramEmailSet ? of(null) : this.loadEmailSettings();
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// If the user navigates to /login from /login, reset the validatedEmail flag
|
||||
// This should bring the user back to the login screen with the email field
|
||||
this.router.events.pipe(takeUntil(this.destroy$)).subscribe((event) => {
|
||||
if (event instanceof NavigationSkipped && event.url === "/login") {
|
||||
this.validatedEmail = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Backup check to handle unknown case where activatedRoute is not available
|
||||
// This shouldn't happen under normal circumstances
|
||||
if (!this.route) {
|
||||
await this.loadEmailSettings();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async submit(showToast = true) {
|
||||
await this.setupCaptcha();
|
||||
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
//web
|
||||
if (this.formGroup.invalid && !showToast) {
|
||||
return;
|
||||
}
|
||||
|
||||
//desktop, browser; This should be removed once all clients use reactive forms
|
||||
if (this.formGroup.invalid && showToast) {
|
||||
const errorText = this.getErrorToastMessage();
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: errorText,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
this.formGroup.controls.email.value,
|
||||
this.formGroup.controls.masterPassword.value,
|
||||
this.captchaToken,
|
||||
undefined,
|
||||
);
|
||||
|
||||
this.formPromise = this.loginStrategyService.logIn(credentials);
|
||||
const response = await this.formPromise;
|
||||
|
||||
await this.saveEmailSettings();
|
||||
|
||||
if (this.handleCaptchaRequired(response)) {
|
||||
return;
|
||||
} else if (await this.handleMigrateEncryptionKey(response)) {
|
||||
return;
|
||||
} else if (response.requiresTwoFactor) {
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulLoginTwoFactorNavigate();
|
||||
} else {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.twoFactorRoute]);
|
||||
}
|
||||
} else if (response.forcePasswordReset != ForceSetPasswordReason.None) {
|
||||
if (this.onSuccessfulLoginForceResetNavigate != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulLoginForceResetNavigate();
|
||||
} else {
|
||||
this.loginEmailService.clearValues();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.forcePasswordResetRoute]);
|
||||
}
|
||||
} else {
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulLogin();
|
||||
}
|
||||
|
||||
if (this.onSuccessfulLoginNavigate != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulLoginNavigate(response.userId);
|
||||
} else {
|
||||
this.loginEmailService.clearValues();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
if (this.ngZone.isStable) {
|
||||
document.getElementById("masterPassword").focus();
|
||||
} else {
|
||||
this.ngZone.onStable
|
||||
.pipe(take(1))
|
||||
.subscribe(() => document.getElementById("masterPassword").focus());
|
||||
}
|
||||
}
|
||||
|
||||
async startAuthRequestLogin() {
|
||||
this.formGroup.get("masterPassword")?.clearValidators();
|
||||
this.formGroup.get("masterPassword")?.updateValueAndValidity();
|
||||
|
||||
if (!this.formGroup.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.saveEmailSettings();
|
||||
await this.router.navigate(["/login-with-device"]);
|
||||
}
|
||||
|
||||
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
||||
// Save off email for SSO
|
||||
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
|
||||
|
||||
// Generate necessary sso params
|
||||
const passwordOptions: any = {
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
numbers: true,
|
||||
special: false,
|
||||
};
|
||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
|
||||
// Save sso params
|
||||
await this.ssoLoginService.setSsoState(state);
|
||||
await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier);
|
||||
|
||||
// Build URI
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webUrl = env.getWebVaultUrl();
|
||||
|
||||
// Launch browser
|
||||
this.platformUtilsService.launchUri(
|
||||
webUrl +
|
||||
"/#/sso?clientId=" +
|
||||
clientId +
|
||||
"&redirectUri=" +
|
||||
encodeURIComponent(ssoRedirectUri) +
|
||||
"&state=" +
|
||||
state +
|
||||
"&codeChallenge=" +
|
||||
codeChallenge +
|
||||
"&email=" +
|
||||
encodeURIComponent(this.formGroup.controls.email.value),
|
||||
);
|
||||
}
|
||||
|
||||
async validateEmail() {
|
||||
this.formGroup.controls.email.markAsTouched();
|
||||
const emailValid = this.formGroup.get("email").valid;
|
||||
|
||||
if (emailValid) {
|
||||
this.toggleValidateEmail(true);
|
||||
await this.getLoginWithDevice(this.loggedEmail);
|
||||
}
|
||||
}
|
||||
|
||||
toggleValidateEmail(value: boolean) {
|
||||
this.validatedEmail = value;
|
||||
if (!this.validatedEmail) {
|
||||
// Reset master password only when going from validated to not validated
|
||||
// so that autofill can work properly
|
||||
this.formGroup.controls.masterPassword.reset();
|
||||
} else {
|
||||
// Mark MP as untouched so that, when users enter email and hit enter,
|
||||
// the MP field doesn't load with validation errors
|
||||
this.formGroup.controls.masterPassword.markAsUntouched();
|
||||
|
||||
// When email is validated, focus on master password after
|
||||
// waiting for input to be rendered
|
||||
if (this.ngZone.isStable) {
|
||||
this.masterPasswordInput?.nativeElement?.focus();
|
||||
} else {
|
||||
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
|
||||
this.masterPasswordInput?.nativeElement?.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadEmailSettings() {
|
||||
// Try to load from memory first
|
||||
const email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
const rememberEmail = this.loginEmailService.getRememberEmail();
|
||||
|
||||
if (email) {
|
||||
this.formGroup.controls.email.setValue(email);
|
||||
this.formGroup.controls.rememberEmail.setValue(rememberEmail);
|
||||
} else {
|
||||
// If not in memory, check email on disk
|
||||
const storedEmail = await firstValueFrom(this.loginEmailService.storedEmail$);
|
||||
if (storedEmail) {
|
||||
// If we have a stored email, rememberEmail should default to true
|
||||
this.formGroup.controls.email.setValue(storedEmail);
|
||||
this.formGroup.controls.rememberEmail.setValue(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async saveEmailSettings() {
|
||||
// Save off email for SSO
|
||||
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
|
||||
|
||||
this.loginEmailService.setLoginEmail(this.formGroup.value.email);
|
||||
this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail);
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
}
|
||||
|
||||
// Legacy accounts used the master key to encrypt data. Migration is required but only performed on web
|
||||
protected async handleMigrateEncryptionKey(result: AuthResult): Promise<boolean> {
|
||||
if (!result.requiresEncryptionKeyMigration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccured"),
|
||||
message: this.i18nService.t("encryptionKeyMigrationRequired"),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private getErrorToastMessage() {
|
||||
const error: AllValidationErrors = this.formValidationErrorService
|
||||
.getFormValidationErrors(this.formGroup.controls)
|
||||
.shift();
|
||||
|
||||
if (error) {
|
||||
switch (error.errorName) {
|
||||
case "email":
|
||||
return this.i18nService.t("invalidEmail");
|
||||
case "minlength":
|
||||
return this.i18nService.t("masterPasswordMinlength", Utils.originalMinimumPasswordLength);
|
||||
default:
|
||||
return this.i18nService.t(this.errorTag(error));
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private errorTag(error: AllValidationErrors): string {
|
||||
const name = error.errorName.charAt(0).toUpperCase() + error.errorName.slice(1);
|
||||
return `${error.controlName}${name}`;
|
||||
}
|
||||
|
||||
async getLoginWithDevice(email: string) {
|
||||
try {
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
this.showLoginWithDevice = await this.devicesApiService.getKnownDevice(
|
||||
email,
|
||||
deviceIdentifier,
|
||||
);
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
this.showLoginWithDevice = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,539 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||
import { IsActiveMatchOptions, Router } from "@angular/router";
|
||||
import { Subject, firstValueFrom, map, takeUntil } from "rxjs";
|
||||
|
||||
import {
|
||||
AuthRequestLoginCredentials,
|
||||
AuthRequestServiceAbstraction,
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums/http-status-code.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
||||
|
||||
enum State {
|
||||
StandardAuthRequest,
|
||||
AdminAuthRequest,
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export class LoginViaAuthRequestComponentV1
|
||||
extends CaptchaProtectedComponent
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private destroy$ = new Subject<void>();
|
||||
userAuthNStatus: AuthenticationStatus;
|
||||
email: string;
|
||||
showResendNotification = false;
|
||||
authRequest: AuthRequest;
|
||||
fingerprintPhrase: string;
|
||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||
onSuccessfulLogin: () => Promise<any>;
|
||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||
|
||||
protected adminApprovalRoute = "admin-approval-requested";
|
||||
|
||||
protected StateEnum = State;
|
||||
protected state = State.StandardAuthRequest;
|
||||
protected webVaultUrl: string;
|
||||
protected twoFactorRoute = "2fa";
|
||||
protected successRoute = "vault";
|
||||
protected forcePasswordResetRoute = "update-temp-password";
|
||||
private resendTimeout = 12000;
|
||||
protected deviceManagementUrl: string;
|
||||
|
||||
private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array };
|
||||
|
||||
constructor(
|
||||
protected router: Router,
|
||||
private keyService: KeyService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
private appIdService: AppIdService,
|
||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private apiService: ApiService,
|
||||
private authService: AuthService,
|
||||
private logService: LogService,
|
||||
environmentService: EnvironmentService,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
private anonymousHubService: AnonymousHubService,
|
||||
private validationService: ValidationService,
|
||||
private accountService: AccountService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
protected toastService: ToastService,
|
||||
) {
|
||||
super(environmentService, i18nService, platformUtilsService, toastService);
|
||||
|
||||
// Get the web vault URL from the environment service
|
||||
environmentService.environment$.pipe(takeUntil(this.destroy$)).subscribe((env) => {
|
||||
this.webVaultUrl = env.getWebVaultUrl();
|
||||
this.deviceManagementUrl = `${this.webVaultUrl}/#/settings/security/device-management`;
|
||||
});
|
||||
|
||||
// Gets signalR push notification
|
||||
// Only fires on approval to prevent enumeration
|
||||
this.authRequestService.authRequestPushNotification$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((id) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.verifyAndHandleApprovedAuthReq(id).catch((e: Error) => {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("error"),
|
||||
message: e.message,
|
||||
});
|
||||
this.logService.error("Failed to use approved auth request: " + e.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
this.userAuthNStatus = await this.authService.getAuthStatus();
|
||||
|
||||
const matchOptions: IsActiveMatchOptions = {
|
||||
paths: "exact",
|
||||
queryParams: "ignored",
|
||||
fragment: "ignored",
|
||||
matrixParams: "ignored",
|
||||
};
|
||||
|
||||
if (this.router.isActive(this.adminApprovalRoute, matchOptions)) {
|
||||
this.state = State.AdminAuthRequest;
|
||||
}
|
||||
|
||||
if (this.state === State.AdminAuthRequest) {
|
||||
// Pull email from state for admin auth reqs b/c it is available
|
||||
// This also prevents it from being lost on refresh as the
|
||||
// login service email does not persist.
|
||||
this.email = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
|
||||
if (!this.email) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("userEmailMissing"),
|
||||
});
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/login-initiated"]);
|
||||
return;
|
||||
}
|
||||
|
||||
// We only allow a single admin approval request to be active at a time
|
||||
// so must check state to see if we have an existing one or not
|
||||
const adminAuthReqStorable = await this.authRequestService.getAdminAuthRequest(userId);
|
||||
|
||||
if (adminAuthReqStorable) {
|
||||
await this.handleExistingAdminAuthRequest(adminAuthReqStorable, userId);
|
||||
} else {
|
||||
// No existing admin auth request; so we need to create one
|
||||
await this.startAuthRequestLogin();
|
||||
}
|
||||
} else {
|
||||
// Standard auth request
|
||||
// TODO: evaluate if we can remove the setting of this.email in the constructor
|
||||
this.email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
|
||||
if (!this.email) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("userEmailMissing"),
|
||||
});
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/login"]);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.startAuthRequestLogin();
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
await this.anonymousHubService.stopHubConnection();
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private async handleExistingAdminAuthRequest(
|
||||
adminAuthReqStorable: AdminAuthRequestStorable,
|
||||
userId: UserId,
|
||||
) {
|
||||
// Note: on login, the SSOLoginStrategy will also call to see an existing admin auth req
|
||||
// has been approved and handle it if so.
|
||||
|
||||
// Regardless, we always retrieve the auth request from the server verify and handle status changes here as well
|
||||
let adminAuthReqResponse: AuthRequestResponse;
|
||||
try {
|
||||
adminAuthReqResponse = await this.apiService.getAuthRequest(adminAuthReqStorable.id);
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
|
||||
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
||||
}
|
||||
}
|
||||
|
||||
// Request doesn't exist anymore
|
||||
if (!adminAuthReqResponse) {
|
||||
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
||||
}
|
||||
|
||||
// Re-derive the user's fingerprint phrase
|
||||
// It is important to not use the server's public key here as it could have been compromised via MITM
|
||||
const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey(
|
||||
adminAuthReqStorable.privateKey,
|
||||
);
|
||||
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||
this.email,
|
||||
derivedPublicKeyArrayBuffer,
|
||||
);
|
||||
|
||||
// Request denied
|
||||
if (adminAuthReqResponse.isAnswered && !adminAuthReqResponse.requestApproved) {
|
||||
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
||||
}
|
||||
|
||||
// Request approved
|
||||
if (adminAuthReqResponse.requestApproved) {
|
||||
return await this.handleApprovedAdminAuthRequest(
|
||||
adminAuthReqResponse,
|
||||
adminAuthReqStorable.privateKey,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
// Request still pending response from admin
|
||||
// set keypair and create hub connection so that any approvals will be received via push notification
|
||||
this.authRequestKeyPair = { privateKey: adminAuthReqStorable.privateKey, publicKey: null };
|
||||
await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id);
|
||||
}
|
||||
|
||||
private async handleExistingAdminAuthReqDeletedOrDenied(userId: UserId) {
|
||||
// clear the admin auth request from state
|
||||
await this.authRequestService.clearAdminAuthRequest(userId);
|
||||
|
||||
// start new auth request
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.startAuthRequestLogin();
|
||||
}
|
||||
|
||||
private async buildAuthRequest(authRequestType: AuthRequestType) {
|
||||
const authRequestKeyPairArray = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
||||
|
||||
this.authRequestKeyPair = {
|
||||
publicKey: authRequestKeyPairArray[0],
|
||||
privateKey: authRequestKeyPairArray[1],
|
||||
};
|
||||
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey);
|
||||
const accessCode = await this.passwordGenerationService.generatePassword({
|
||||
type: "password",
|
||||
length: 25,
|
||||
});
|
||||
|
||||
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||
this.email,
|
||||
this.authRequestKeyPair.publicKey,
|
||||
);
|
||||
|
||||
this.authRequest = new AuthRequest(
|
||||
this.email,
|
||||
deviceIdentifier,
|
||||
publicKey,
|
||||
authRequestType,
|
||||
accessCode,
|
||||
);
|
||||
}
|
||||
|
||||
async startAuthRequestLogin() {
|
||||
this.showResendNotification = false;
|
||||
|
||||
try {
|
||||
let reqResponse: AuthRequestResponse;
|
||||
|
||||
if (this.state === State.AdminAuthRequest) {
|
||||
await this.buildAuthRequest(AuthRequestType.AdminApproval);
|
||||
reqResponse = await this.apiService.postAdminAuthRequest(this.authRequest);
|
||||
|
||||
const adminAuthReqStorable = new AdminAuthRequestStorable({
|
||||
id: reqResponse.id,
|
||||
privateKey: this.authRequestKeyPair.privateKey,
|
||||
});
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
await this.authRequestService.setAdminAuthRequest(adminAuthReqStorable, userId);
|
||||
} else {
|
||||
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
|
||||
reqResponse = await this.apiService.postAuthRequest(this.authRequest);
|
||||
}
|
||||
|
||||
if (reqResponse.id) {
|
||||
await this.anonymousHubService.createHubConnection(reqResponse.id);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.showResendNotification = true;
|
||||
}, this.resendTimeout);
|
||||
}
|
||||
|
||||
private async verifyAndHandleApprovedAuthReq(requestId: string) {
|
||||
try {
|
||||
// Retrieve the auth request from server and verify it's approved
|
||||
let authReqResponse: AuthRequestResponse;
|
||||
|
||||
switch (this.state) {
|
||||
case State.StandardAuthRequest:
|
||||
// Unauthed - access code required for user verification
|
||||
authReqResponse = await this.apiService.getAuthResponse(
|
||||
requestId,
|
||||
this.authRequest.accessCode,
|
||||
);
|
||||
break;
|
||||
|
||||
case State.AdminAuthRequest:
|
||||
// Authed - no access code required
|
||||
authReqResponse = await this.apiService.getAuthRequest(requestId);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!authReqResponse.requestApproved) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Approved so proceed:
|
||||
|
||||
// 4 Scenarios to handle for approved auth requests:
|
||||
// Existing flow 1:
|
||||
// - Anon Login with Device > User is not AuthN > receives approval from device with pubKey(masterKey)
|
||||
// > decrypt masterKey > must authenticate > gets masterKey(userKey) > decrypt userKey and proceed to vault
|
||||
|
||||
// 3 new flows from TDE:
|
||||
// Flow 2:
|
||||
// - Post SSO > User is AuthN > SSO login strategy success sets masterKey(userKey) > receives approval from device with pubKey(masterKey)
|
||||
// > decrypt masterKey > decrypt userKey > establish trust if required > proceed to vault
|
||||
// Flow 3:
|
||||
// - Post SSO > User is AuthN > Receives approval from device with pubKey(userKey) > decrypt userKey > establish trust if required > proceed to vault
|
||||
// Flow 4:
|
||||
// - Anon Login with Device > User is not AuthN > receives approval from device with pubKey(userKey)
|
||||
// > decrypt userKey > must authenticate > set userKey > proceed to vault
|
||||
|
||||
// if user has authenticated via SSO
|
||||
if (this.userAuthNStatus === AuthenticationStatus.Locked) {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
return await this.handleApprovedAdminAuthRequest(
|
||||
authReqResponse,
|
||||
this.authRequestKeyPair.privateKey,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
// Flow 1 and 4:
|
||||
const loginAuthResult = await this.loginViaAuthRequestStrategy(requestId, authReqResponse);
|
||||
await this.handlePostLoginNavigation(loginAuthResult);
|
||||
} catch (error) {
|
||||
if (error instanceof ErrorResponse) {
|
||||
let errorRoute = "/login";
|
||||
if (this.state === State.AdminAuthRequest) {
|
||||
errorRoute = "/login-initiated";
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([errorRoute]);
|
||||
this.validationService.showError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async handleApprovedAdminAuthRequest(
|
||||
adminAuthReqResponse: AuthRequestResponse,
|
||||
privateKey: ArrayBuffer,
|
||||
userId: UserId,
|
||||
) {
|
||||
// See verifyAndHandleApprovedAuthReq(...) for flow details
|
||||
// it's flow 2 or 3 based on presence of masterPasswordHash
|
||||
if (adminAuthReqResponse.masterPasswordHash) {
|
||||
// Flow 2: masterPasswordHash is not null
|
||||
// key is authRequestPublicKey(masterKey) + we have authRequestPublicKey(masterPasswordHash)
|
||||
await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash(
|
||||
adminAuthReqResponse,
|
||||
privateKey,
|
||||
userId,
|
||||
);
|
||||
} else {
|
||||
// Flow 3: masterPasswordHash is null
|
||||
// we can assume key is authRequestPublicKey(userKey) and we can just decrypt with userKey and proceed to vault
|
||||
await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey(
|
||||
adminAuthReqResponse,
|
||||
privateKey,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
// clear the admin auth request from state so it cannot be used again (it's a one time use)
|
||||
// TODO: this should eventually be enforced via deleting this on the server once it is used
|
||||
await this.authRequestService.clearAdminAuthRequest(userId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("loginApproved"),
|
||||
});
|
||||
|
||||
// Now that we have a decrypted user key in memory, we can check if we
|
||||
// need to establish trust on the current device
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id);
|
||||
|
||||
// TODO: don't forget to use auto enrollment service everywhere we trust device
|
||||
|
||||
await this.handleSuccessfulLoginNavigation();
|
||||
}
|
||||
|
||||
// Authentication helper
|
||||
private async buildAuthRequestLoginCredentials(
|
||||
requestId: string,
|
||||
response: AuthRequestResponse,
|
||||
): Promise<AuthRequestLoginCredentials> {
|
||||
// if masterPasswordHash has a value, we will always receive key as authRequestPublicKey(masterKey) + authRequestPublicKey(masterPasswordHash)
|
||||
// if masterPasswordHash is null, we will always receive key as authRequestPublicKey(userKey)
|
||||
if (response.masterPasswordHash) {
|
||||
const { masterKey, masterKeyHash } =
|
||||
await this.authRequestService.decryptPubKeyEncryptedMasterKeyAndHash(
|
||||
response.key,
|
||||
response.masterPasswordHash,
|
||||
this.authRequestKeyPair.privateKey,
|
||||
);
|
||||
|
||||
return new AuthRequestLoginCredentials(
|
||||
this.email,
|
||||
this.authRequest.accessCode,
|
||||
requestId,
|
||||
null, // no userKey
|
||||
masterKey,
|
||||
masterKeyHash,
|
||||
);
|
||||
} else {
|
||||
const userKey = await this.authRequestService.decryptPubKeyEncryptedUserKey(
|
||||
response.key,
|
||||
this.authRequestKeyPair.privateKey,
|
||||
);
|
||||
return new AuthRequestLoginCredentials(
|
||||
this.email,
|
||||
this.authRequest.accessCode,
|
||||
requestId,
|
||||
userKey,
|
||||
null, // no masterKey
|
||||
null, // no masterKeyHash
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async loginViaAuthRequestStrategy(
|
||||
requestId: string,
|
||||
authReqResponse: AuthRequestResponse,
|
||||
): Promise<AuthResult> {
|
||||
// Note: credentials change based on if the authReqResponse.key is a encryptedMasterKey or UserKey
|
||||
const credentials = await this.buildAuthRequestLoginCredentials(requestId, authReqResponse);
|
||||
|
||||
// Note: keys are set by AuthRequestLoginStrategy success handling
|
||||
return await this.loginStrategyService.logIn(credentials);
|
||||
}
|
||||
|
||||
// Routing logic
|
||||
private async handlePostLoginNavigation(loginResponse: AuthResult) {
|
||||
if (loginResponse.requiresTwoFactor) {
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulLoginTwoFactorNavigate();
|
||||
} else {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.twoFactorRoute]);
|
||||
}
|
||||
} else if (loginResponse.forcePasswordReset != ForceSetPasswordReason.None) {
|
||||
if (this.onSuccessfulLoginForceResetNavigate != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulLoginForceResetNavigate();
|
||||
} else {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.forcePasswordResetRoute]);
|
||||
}
|
||||
} else {
|
||||
await this.handleSuccessfulLoginNavigation();
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSuccessfulLoginNavigation() {
|
||||
if (this.state === State.StandardAuthRequest) {
|
||||
// Only need to set remembered email on standard login with auth req flow
|
||||
await this.loginEmailService.saveEmailSettings();
|
||||
}
|
||||
|
||||
if (this.onSuccessfulLogin != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulLogin();
|
||||
}
|
||||
|
||||
if (this.onSuccessfulLoginNavigate != null) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.onSuccessfulLoginNavigate();
|
||||
} else {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate([this.successRoute]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { firstValueFrom, map } from "rxjs";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
@@ -17,11 +17,12 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -62,6 +63,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
policyService: PolicyService,
|
||||
protected router: Router,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private apiService: ApiService,
|
||||
private syncService: SyncService,
|
||||
private route: ActivatedRoute,
|
||||
@@ -195,7 +197,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
);
|
||||
try {
|
||||
if (this.resetPasswordAutoEnroll) {
|
||||
this.formPromise = this.apiService
|
||||
this.formPromise = this.masterPasswordApiService
|
||||
.setPassword(request)
|
||||
.then(async () => {
|
||||
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
||||
@@ -222,7 +224,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent implements
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.apiService.setPassword(request).then(async () => {
|
||||
this.formPromise = this.masterPasswordApiService.setPassword(request).then(async () => {
|
||||
await this.onSetPasswordSuccess(masterKey, userKey, newKeyPair);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ import {
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
@@ -14,11 +14,11 @@ import {
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ActivatedRoute, convertToParamMap, Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
@@ -16,13 +15,13 @@ import {
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
@@ -17,7 +16,6 @@ import {
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
@@ -28,6 +26,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
|
||||
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
|
||||
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
|
||||
import { WebAuthnIFrame } from "@bitwarden/common/auth/webauthn-iframe";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
import { Directive } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { Verification } from "@bitwarden/common/auth/types/verification";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -40,7 +40,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||
policyService: PolicyService,
|
||||
keyService: KeyService,
|
||||
messagingService: MessagingService,
|
||||
private apiService: ApiService,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private logService: LogService,
|
||||
dialogService: DialogService,
|
||||
@@ -117,9 +117,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||
request.key = newUserKey[1].encryptedString;
|
||||
|
||||
// Update user's password
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.apiService.postPassword(request);
|
||||
await this.masterPasswordApiService.postPassword(request);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
|
||||
@@ -4,11 +4,10 @@ import { Directive, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
@@ -16,6 +15,7 @@ import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "@bitwarden/common/auth/models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
|
||||
import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -52,7 +52,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
|
||||
policyService: PolicyService,
|
||||
keyService: KeyService,
|
||||
messagingService: MessagingService,
|
||||
private apiService: ApiService,
|
||||
private masterPasswordApiService: MasterPasswordApiService,
|
||||
private syncService: SyncService,
|
||||
private logService: LogService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
@@ -202,7 +202,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.masterPasswordHint = this.hint;
|
||||
|
||||
return this.apiService.putUpdateTempPassword(request);
|
||||
return this.masterPasswordApiService.putUpdateTempPassword(request);
|
||||
}
|
||||
|
||||
private async updatePassword(newMasterPasswordHash: string, userKey: [UserKey, EncString]) {
|
||||
@@ -214,7 +214,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
|
||||
request.newMasterPasswordHash = newMasterPasswordHash;
|
||||
request.key = userKey[1].encryptedString;
|
||||
|
||||
return this.apiService.postPassword(request);
|
||||
return this.masterPasswordApiService.postPassword(request);
|
||||
}
|
||||
|
||||
private async updateTdeOffboardingPassword(
|
||||
@@ -226,6 +226,6 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent imp
|
||||
request.newMasterPasswordHash = masterPasswordHash;
|
||||
request.masterPasswordHint = this.hint;
|
||||
|
||||
return this.apiService.putUpdateTdeOffboardingPassword(request);
|
||||
return this.masterPasswordApiService.putUpdateTdeOffboardingPassword(request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
@Directive({
|
||||
selector: "app-user-verification",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class UserVerificationComponent implements ControlValueAccessor, OnInit, OnDestroy {
|
||||
private _invalidSecret = false;
|
||||
@Input()
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { Navigation, Router, UrlTree } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { unauthUiRefreshRedirect } from "./unauth-ui-refresh-redirect";
|
||||
|
||||
describe("unauthUiRefreshRedirect", () => {
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let router: MockProxy<Router>;
|
||||
|
||||
beforeEach(() => {
|
||||
configService = mock<ConfigService>();
|
||||
router = mock<Router>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns true when UnauthenticatedExtensionUIRefresh flag is disabled", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
const result = await TestBed.runInInjectionContext(() =>
|
||||
unauthUiRefreshRedirect("/redirect")(),
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.UnauthenticatedExtensionUIRefresh,
|
||||
);
|
||||
expect(router.parseUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns UrlTree when UnauthenticatedExtensionUIRefresh flag is enabled and preserves query params", async () => {
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
|
||||
const urlTree = new UrlTree();
|
||||
urlTree.queryParams = { test: "test" };
|
||||
|
||||
const navigation: Navigation = {
|
||||
extras: {},
|
||||
id: 0,
|
||||
initialUrl: new UrlTree(),
|
||||
extractedUrl: urlTree,
|
||||
trigger: "imperative",
|
||||
previousNavigation: undefined,
|
||||
};
|
||||
|
||||
router.getCurrentNavigation.mockReturnValue(navigation);
|
||||
|
||||
await TestBed.runInInjectionContext(() => unauthUiRefreshRedirect("/redirect")());
|
||||
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.UnauthenticatedExtensionUIRefresh,
|
||||
);
|
||||
expect(router.createUrlTree).toHaveBeenCalledWith(["/redirect"], {
|
||||
queryParams: urlTree.queryParams,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { inject } from "@angular/core";
|
||||
import { UrlTree, Router } from "@angular/router";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
/**
|
||||
* Helper function to redirect to a new URL based on the UnauthenticatedExtensionUIRefresh feature flag.
|
||||
* @param redirectUrl - The URL to redirect to if the UnauthenticatedExtensionUIRefresh flag is enabled.
|
||||
*/
|
||||
export function unauthUiRefreshRedirect(redirectUrl: string): () => Promise<boolean | UrlTree> {
|
||||
return async () => {
|
||||
const configService = inject(ConfigService);
|
||||
const router = inject(Router);
|
||||
const shouldRedirect = await configService.getFeatureFlag(
|
||||
FeatureFlag.UnauthenticatedExtensionUIRefresh,
|
||||
);
|
||||
if (shouldRedirect) {
|
||||
const currentNavigation = router.getCurrentNavigation();
|
||||
const queryParams = currentNavigation?.extractedUrl?.queryParams || {};
|
||||
|
||||
// Preserve query params when redirecting as it is likely that the refreshed component
|
||||
// will be consuming the same query params.
|
||||
return router.createUrlTree([redirectUrl], { queryParams });
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
AccountService,
|
||||
} from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
export const authGuard: CanActivateFn = async (
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
AccountService,
|
||||
} from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@@ -9,10 +9,10 @@ import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
|
||||
@@ -3,8 +3,8 @@ import { CanActivateFn, Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import { BehaviorSubject } from "rxjs";
|
||||
import { EmptyComponent } from "@bitwarden/angular/platform/guard/feature-flag.guard.spec";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -4,8 +4,8 @@ import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export abstract class DeviceTrustToastService {
|
||||
/**
|
||||
* An observable pipeline that observes any cross-application toast messages
|
||||
* that need to be shown as part of the trusted device encryption (TDE) process.
|
||||
*/
|
||||
abstract setupListeners$: Observable<void>;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { merge, Observable, tap } from "rxjs";
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "./device-trust-toast.service.abstraction";
|
||||
|
||||
export class DeviceTrustToastService implements DeviceTrustToastServiceAbstraction {
|
||||
private adminLoginApproved$: Observable<void>;
|
||||
private deviceTrusted$: Observable<void>;
|
||||
|
||||
setupListeners$: Observable<void>;
|
||||
|
||||
constructor(
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
private i18nService: I18nService,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
this.adminLoginApproved$ = this.authRequestService.adminLoginApproved$.pipe(
|
||||
tap(() => {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("loginApproved"),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
this.deviceTrusted$ = this.deviceTrustService.deviceTrusted$.pipe(
|
||||
tap(() => {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("deviceTrusted"),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
this.setupListeners$ = merge(this.adminLoginApproved$, this.deviceTrusted$);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { EMPTY, of } from "rxjs";
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "./device-trust-toast.service.abstraction";
|
||||
import { DeviceTrustToastService } from "./device-trust-toast.service.implementation";
|
||||
|
||||
describe("DeviceTrustToastService", () => {
|
||||
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
|
||||
let deviceTrustService: MockProxy<DeviceTrustServiceAbstraction>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
|
||||
let sut: DeviceTrustToastServiceAbstraction;
|
||||
|
||||
beforeEach(() => {
|
||||
authRequestService = mock<AuthRequestServiceAbstraction>();
|
||||
deviceTrustService = mock<DeviceTrustServiceAbstraction>();
|
||||
i18nService = mock<I18nService>();
|
||||
toastService = mock<ToastService>();
|
||||
|
||||
i18nService.t.mockImplementation((key: string) => key); // just return the key that was given
|
||||
});
|
||||
|
||||
const initService = () => {
|
||||
return new DeviceTrustToastService(
|
||||
authRequestService,
|
||||
deviceTrustService,
|
||||
i18nService,
|
||||
toastService,
|
||||
);
|
||||
};
|
||||
|
||||
const loginApprovalToastOptions = {
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: "loginApproved",
|
||||
};
|
||||
|
||||
const deviceTrustedToastOptions = {
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: "deviceTrusted",
|
||||
};
|
||||
|
||||
describe("setupListeners$", () => {
|
||||
describe("given adminLoginApproved$ emits and deviceTrusted$ emits", () => {
|
||||
beforeEach(() => {
|
||||
// Arrange
|
||||
authRequestService.adminLoginApproved$ = of(undefined);
|
||||
deviceTrustService.deviceTrusted$ = of(undefined);
|
||||
sut = initService();
|
||||
});
|
||||
|
||||
it("should trigger a toast for login approval", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).toHaveBeenCalledWith(loginApprovalToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should trigger a toast for device trust", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given adminLoginApproved$ emits and deviceTrusted$ does not emit", () => {
|
||||
beforeEach(() => {
|
||||
// Arrange
|
||||
authRequestService.adminLoginApproved$ = of(undefined);
|
||||
deviceTrustService.deviceTrusted$ = EMPTY;
|
||||
sut = initService();
|
||||
});
|
||||
|
||||
it("should trigger a toast for login approval", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).toHaveBeenCalledWith(loginApprovalToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT trigger a toast for device trust", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).not.toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given adminLoginApproved$ does not emit and deviceTrusted$ emits", () => {
|
||||
beforeEach(() => {
|
||||
// Arrange
|
||||
authRequestService.adminLoginApproved$ = EMPTY;
|
||||
deviceTrustService.deviceTrusted$ = of(undefined);
|
||||
sut = initService();
|
||||
});
|
||||
|
||||
it("should NOT trigger a toast for login approval", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).not.toHaveBeenCalledWith(loginApprovalToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should trigger a toast for device trust", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("given adminLoginApproved$ does not emit and deviceTrusted$ does not emit", () => {
|
||||
beforeEach(() => {
|
||||
// Arrange
|
||||
authRequestService.adminLoginApproved$ = EMPTY;
|
||||
deviceTrustService.deviceTrusted$ = EMPTY;
|
||||
sut = initService();
|
||||
});
|
||||
|
||||
it("should NOT trigger a toast for login approval", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).not.toHaveBeenCalledWith(loginApprovalToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should NOT trigger a toast for device trust", (done) => {
|
||||
// Act
|
||||
sut.setupListeners$.subscribe({
|
||||
complete: () => {
|
||||
expect(toastService.showToast).not.toHaveBeenCalledWith(deviceTrustedToastOptions); // Assert
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<bit-table *ngIf="!loading">
|
||||
<ng-container header>
|
||||
|
||||
@@ -59,8 +59,6 @@ export class AngularThemingService implements AbstractThemingService {
|
||||
document.documentElement.classList.remove(
|
||||
"theme_" + ThemeTypes.Light,
|
||||
"theme_" + ThemeTypes.Dark,
|
||||
"theme_" + ThemeTypes.Nord,
|
||||
"theme_" + ThemeTypes.SolarizedDark,
|
||||
);
|
||||
document.documentElement.classList.add("theme_" + theme);
|
||||
});
|
||||
|
||||
@@ -87,14 +87,9 @@ import {
|
||||
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||
import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import {
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
MasterPasswordServiceAbstraction,
|
||||
} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordApiService as MasterPasswordApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
@@ -109,11 +104,9 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||
import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation";
|
||||
import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation";
|
||||
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service";
|
||||
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
|
||||
import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation";
|
||||
import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/services/token.service";
|
||||
@@ -153,6 +146,15 @@ import { BulkEncryptService } from "@bitwarden/common/key-management/crypto/abst
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { BulkEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/bulk-encrypt.service.implementation";
|
||||
import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/multithread-encrypt.service.implementation";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { DeviceTrustService } from "@bitwarden/common/key-management/device-trust/services/device-trust.service.implementation";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/services/key-connector.service";
|
||||
import {
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
MasterPasswordServiceAbstraction,
|
||||
} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordService } from "@bitwarden/common/key-management/master-password/services/master-password.service";
|
||||
import {
|
||||
DefaultVaultTimeoutService,
|
||||
DefaultVaultTimeoutSettingsService,
|
||||
@@ -304,6 +306,8 @@ import {
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
import {
|
||||
DefaultTaskService,
|
||||
DefaultEndUserNotificationService,
|
||||
EndUserNotificationService,
|
||||
NewDeviceVerificationNoticeService,
|
||||
PasswordRepromptService,
|
||||
TaskService,
|
||||
@@ -317,6 +321,8 @@ import {
|
||||
IndividualVaultExportServiceAbstraction,
|
||||
} from "@bitwarden/vault-export-core";
|
||||
|
||||
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
|
||||
import { DeviceTrustToastService } from "../auth/services/device-trust-toast.service.implementation";
|
||||
import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service";
|
||||
import { ViewCacheService } from "../platform/abstractions/view-cache.service";
|
||||
import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service";
|
||||
@@ -408,7 +414,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: ThemeStateService,
|
||||
useClass: DefaultThemeStateService,
|
||||
deps: [GlobalStateProvider, ConfigService],
|
||||
deps: [GlobalStateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AbstractThemingService,
|
||||
@@ -603,7 +609,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: TotpServiceAbstraction,
|
||||
useClass: TotpService,
|
||||
deps: [CryptoFunctionServiceAbstraction, LogService],
|
||||
deps: [SdkService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TokenServiceAbstraction,
|
||||
@@ -1282,7 +1288,7 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: BillingApiServiceAbstraction,
|
||||
useClass: BillingApiService,
|
||||
deps: [ApiServiceAbstraction, LogService, ToastService],
|
||||
deps: [ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TaxServiceAbstraction,
|
||||
@@ -1346,6 +1352,7 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultSetPasswordJitService,
|
||||
deps: [
|
||||
ApiServiceAbstraction,
|
||||
MasterPasswordApiServiceAbstraction,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
I18nServiceAbstraction,
|
||||
@@ -1417,7 +1424,12 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: CipherAuthorizationService,
|
||||
useClass: DefaultCipherAuthorizationService,
|
||||
deps: [CollectionService, OrganizationServiceAbstraction, AccountServiceAbstraction],
|
||||
deps: [
|
||||
CollectionService,
|
||||
OrganizationServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: AuthRequestApiService,
|
||||
@@ -1463,6 +1475,26 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultTaskService,
|
||||
deps: [StateProvider, ApiServiceAbstraction, OrganizationServiceAbstraction, ConfigService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EndUserNotificationService,
|
||||
useClass: DefaultEndUserNotificationService,
|
||||
deps: [StateProvider, ApiServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DeviceTrustToastServiceAbstraction,
|
||||
useClass: DeviceTrustToastService,
|
||||
deps: [
|
||||
AuthRequestServiceAbstraction,
|
||||
DeviceTrustServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
ToastService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MasterPasswordApiServiceAbstraction,
|
||||
useClass: MasterPasswordApiService,
|
||||
deps: [ApiServiceAbstraction, LogService],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -79,9 +81,12 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
protected sendApiService: SendApiService,
|
||||
protected dialogService: DialogService,
|
||||
protected toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this.policyService
|
||||
.policyAppliesToActiveUser$(PolicyType.DisableSend)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
@@ -91,7 +96,7 @@ export class SendComponent implements OnInit, OnDestroy {
|
||||
|
||||
this._searchText$
|
||||
.pipe(
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(userId, searchText))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((isSearchable) => {
|
||||
|
||||
@@ -15,7 +15,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -41,7 +40,7 @@ import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { generate_ssh_key } from "@bitwarden/sdk-internal";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
@Directive()
|
||||
export class AddEditComponent implements OnInit, OnDestroy {
|
||||
@@ -131,7 +130,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
protected configService: ConfigService,
|
||||
protected cipherAuthorizationService: CipherAuthorizationService,
|
||||
protected toastService: ToastService,
|
||||
private sdkService: SdkService,
|
||||
protected sdkService: SdkService,
|
||||
private sshImportPromptService: SshImportPromptService,
|
||||
) {
|
||||
this.typeOptions = [
|
||||
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
|
||||
@@ -207,10 +207,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
this.writeableCollections = await this.loadCollections();
|
||||
this.canUseReprompt = await this.passwordRepromptService.enabled();
|
||||
|
||||
const sshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem);
|
||||
if (sshKeysEnabled) {
|
||||
this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey });
|
||||
}
|
||||
this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey });
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -824,6 +821,15 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
||||
return true;
|
||||
}
|
||||
|
||||
async importSshKeyFromClipboard() {
|
||||
const key = await this.sshImportPromptService.importSshKeyFromClipboard();
|
||||
if (key != null) {
|
||||
this.cipher.sshKey.privateKey = key.privateKey;
|
||||
this.cipher.sshKey.publicKey = key.publicKey;
|
||||
this.cipher.sshKey.keyFingerprint = key.keyFingerprint;
|
||||
}
|
||||
}
|
||||
|
||||
private async generateSshKey(showNotification: boolean = true) {
|
||||
await firstValueFrom(this.sdkService.client$);
|
||||
const sshKey = generate_ssh_key("Ed25519");
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
<ng-container *ngIf="data$ | async as data">
|
||||
<img
|
||||
[src]="data.image"
|
||||
[appFallbackSrc]="data.fallbackImage"
|
||||
*ngIf="data.imageEnabled && data.image"
|
||||
class="tw-size-6 tw-rounded-md"
|
||||
alt=""
|
||||
decoding="async"
|
||||
loading="lazy"
|
||||
[ngClass]="{ 'tw-invisible tw-absolute': !imageLoaded() }"
|
||||
(load)="imageLoaded.set(true)"
|
||||
(error)="imageLoaded.set(false)"
|
||||
/>
|
||||
<i
|
||||
class="tw-w-6 tw-text-muted bwi bwi-lg {{ data.icon }}"
|
||||
*ngIf="!data.imageEnabled || !data.image"
|
||||
*ngIf="!data.imageEnabled || !data.image || !imageLoaded()"
|
||||
></i>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from "@angular/core";
|
||||
import { ChangeDetectionStrategy, Component, input, signal } from "@angular/core";
|
||||
import { toObservable } from "@angular/core/rxjs-interop";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
tap,
|
||||
Observable,
|
||||
startWith,
|
||||
pairwise,
|
||||
} from "rxjs";
|
||||
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
||||
import { buildCipherIcon, CipherIconDetails } from "@bitwarden/common/vault/icon/build-cipher-icon";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
@Component({
|
||||
@@ -20,33 +20,40 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
templateUrl: "icon.component.html",
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class IconComponent implements OnInit {
|
||||
@Input()
|
||||
set cipher(value: CipherView) {
|
||||
this.cipher$.next(value);
|
||||
}
|
||||
export class IconComponent {
|
||||
/**
|
||||
* The cipher to display the icon for.
|
||||
*/
|
||||
cipher = input.required<CipherView>();
|
||||
|
||||
protected data$: Observable<{
|
||||
imageEnabled: boolean;
|
||||
image?: string;
|
||||
fallbackImage: string;
|
||||
icon?: string;
|
||||
}>;
|
||||
imageLoaded = signal(false);
|
||||
|
||||
private cipher$ = new BehaviorSubject<CipherView>(undefined);
|
||||
protected data$: Observable<CipherIconDetails>;
|
||||
|
||||
constructor(
|
||||
private environmentService: EnvironmentService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.data$ = combineLatest([
|
||||
) {
|
||||
const iconSettings$ = combineLatest([
|
||||
this.environmentService.environment$.pipe(map((e) => e.getIconsUrl())),
|
||||
this.domainSettingsService.showFavicons$.pipe(distinctUntilChanged()),
|
||||
this.cipher$.pipe(filter((c) => c !== undefined)),
|
||||
]).pipe(
|
||||
map(([iconsUrl, showFavicon, cipher]) => buildCipherIcon(iconsUrl, cipher, showFavicon)),
|
||||
map(([iconsUrl, showFavicon]) => ({ iconsUrl, showFavicon })),
|
||||
startWith({ iconsUrl: null, showFavicon: false }), // Start with a safe default to avoid flickering icons
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
this.data$ = combineLatest([iconSettings$, toObservable(this.cipher)]).pipe(
|
||||
map(([{ iconsUrl, showFavicon }, cipher]) => buildCipherIcon(iconsUrl, cipher, showFavicon)),
|
||||
startWith(null),
|
||||
pairwise(),
|
||||
tap(([prev, next]) => {
|
||||
if (prev?.image !== next?.image) {
|
||||
// The image changed, reset the loaded state to not show an empty icon
|
||||
this.imageLoaded.set(false);
|
||||
}
|
||||
}),
|
||||
map(([_, next]) => next!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,22 @@
|
||||
// @ts-strict-ignore
|
||||
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { BehaviorSubject, Subject, combineLatest, filter, from, switchMap, takeUntil } from "rxjs";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Subject,
|
||||
combineLatest,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
from,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
} from "rxjs";
|
||||
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
@@ -28,6 +38,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
|
||||
/** Construct filters as an observable so it can be appended to the cipher stream. */
|
||||
private _filter$ = new BehaviorSubject<(cipher: CipherView) => boolean | null>(null);
|
||||
private userId: UserId;
|
||||
private destroy$ = new Subject<void>();
|
||||
private isSearchable: boolean = false;
|
||||
private _searchText$ = new BehaviorSubject<string>("");
|
||||
@@ -55,10 +66,12 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
this.subscribeToCiphers();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
async ngOnInit() {
|
||||
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
|
||||
this._searchText$
|
||||
.pipe(
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
|
||||
switchMap((searchText) => from(this.searchService.isSearchable(this.userId, searchText))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((isSearchable) => {
|
||||
@@ -138,6 +151,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
|
||||
allCiphers = [..._failedCiphers, ...allCiphers];
|
||||
|
||||
return this.searchService.searchCiphers(
|
||||
this.userId,
|
||||
searchText,
|
||||
[filter, this.deletedFilter],
|
||||
allCiphers,
|
||||
|
||||
@@ -51,6 +51,7 @@ import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.v
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { TotpInfo } from "@bitwarden/common/vault/services/totp.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
@@ -87,20 +88,19 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
showPrivateKey: boolean;
|
||||
canAccessPremium: boolean;
|
||||
showPremiumRequiredTotp: boolean;
|
||||
totpCode: string;
|
||||
totpCodeFormatted: string;
|
||||
totpDash: number;
|
||||
totpSec: number;
|
||||
totpLow: boolean;
|
||||
fieldType = FieldType;
|
||||
checkPasswordPromise: Promise<number>;
|
||||
folder: FolderView;
|
||||
cipherType = CipherType;
|
||||
|
||||
private totpInterval: any;
|
||||
private previousCipherId: string;
|
||||
private passwordReprompted = false;
|
||||
|
||||
/**
|
||||
* Represents TOTP information including display formatting and timing
|
||||
*/
|
||||
protected totpInfo$: Observable<TotpInfo> | undefined;
|
||||
|
||||
get fido2CredentialCreationDateValue(): string {
|
||||
const dateCreated = this.i18nService.t("dateCreated");
|
||||
const creationDate = this.datePipe.transform(
|
||||
@@ -504,57 +504,12 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
private cleanUp() {
|
||||
this.totpCode = null;
|
||||
this.cipher = null;
|
||||
this.folder = null;
|
||||
this.showPassword = false;
|
||||
this.showCardNumber = false;
|
||||
this.showCardCode = false;
|
||||
this.passwordReprompted = false;
|
||||
if (this.totpInterval) {
|
||||
clearInterval(this.totpInterval);
|
||||
}
|
||||
}
|
||||
|
||||
private async totpUpdateCode() {
|
||||
if (
|
||||
this.cipher == null ||
|
||||
this.cipher.type !== CipherType.Login ||
|
||||
this.cipher.login.totp == null
|
||||
) {
|
||||
if (this.totpInterval) {
|
||||
clearInterval(this.totpInterval);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.totpCode = await this.totpService.getCode(this.cipher.login.totp);
|
||||
if (this.totpCode != null) {
|
||||
if (this.totpCode.length > 4) {
|
||||
const half = Math.floor(this.totpCode.length / 2);
|
||||
this.totpCodeFormatted =
|
||||
this.totpCode.substring(0, half) + " " + this.totpCode.substring(half);
|
||||
} else {
|
||||
this.totpCodeFormatted = this.totpCode;
|
||||
}
|
||||
} else {
|
||||
this.totpCodeFormatted = null;
|
||||
if (this.totpInterval) {
|
||||
clearInterval(this.totpInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async totpTick(intervalSeconds: number) {
|
||||
const epoch = Math.round(new Date().getTime() / 1000.0);
|
||||
const mod = epoch % intervalSeconds;
|
||||
|
||||
this.totpSec = intervalSeconds - mod;
|
||||
this.totpDash = +(Math.round(((78.6 / intervalSeconds) * mod + "e+2") as any) + "e-2");
|
||||
this.totpLow = this.totpSec <= 7;
|
||||
if (mod === 0) {
|
||||
await this.totpUpdateCode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -577,19 +532,33 @@ export class ViewComponent implements OnDestroy, OnInit {
|
||||
).find((f) => f.id == this.cipher.folderId);
|
||||
}
|
||||
|
||||
if (
|
||||
const canGenerateTotp =
|
||||
this.cipher.type === CipherType.Login &&
|
||||
this.cipher.login.totp &&
|
||||
(this.cipher.organizationUseTotp || this.canAccessPremium)
|
||||
) {
|
||||
await this.totpUpdateCode();
|
||||
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
|
||||
await this.totpTick(interval);
|
||||
(this.cipher.organizationUseTotp || this.canAccessPremium);
|
||||
|
||||
this.totpInterval = setInterval(async () => {
|
||||
await this.totpTick(interval);
|
||||
}, 1000);
|
||||
}
|
||||
this.totpInfo$ = canGenerateTotp
|
||||
? this.totpService.getCode$(this.cipher.login.totp).pipe(
|
||||
map((response) => {
|
||||
const epoch = Math.round(new Date().getTime() / 1000.0);
|
||||
const mod = epoch % response.period;
|
||||
|
||||
// Format code
|
||||
const totpCodeFormatted =
|
||||
response.code.length > 4
|
||||
? `${response.code.slice(0, Math.floor(response.code.length / 2))} ${response.code.slice(Math.floor(response.code.length / 2))}`
|
||||
: response.code;
|
||||
|
||||
return {
|
||||
totpCode: response.code,
|
||||
totpCodeFormatted,
|
||||
totpDash: +(Math.round(((78.6 / response.period) * mod + "e+2") as any) + "e-2"),
|
||||
totpSec: response.period - mod,
|
||||
totpLow: response.period - mod <= 7,
|
||||
} as TotpInfo;
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (this.previousCipherId !== this.cipherId) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
"@bitwarden/generator-history": ["../tools/generator/extensions/history/src"],
|
||||
"@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"],
|
||||
"@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"],
|
||||
"@bitwarden/importer/core": ["../importer/src"],
|
||||
"@bitwarden/importer-ui": ["../importer/src/components"],
|
||||
"@bitwarden/key-management": ["../key-management/src"],
|
||||
"@bitwarden/platform": ["../platform/src"],
|
||||
"@bitwarden/ui-common": ["../ui/common/src"],
|
||||
|
||||
@@ -18,8 +18,11 @@
|
||||
<p>{{ authRequestResponse?.requestDeviceType }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<b>{{ "ipAddress" | i18n }}</b>
|
||||
<p>{{ authRequestResponse?.requestIpAddress }}</p>
|
||||
<b>{{ "location" | i18n }}</b>
|
||||
<p>
|
||||
<span class="tw-capitalize">{{ authRequestResponse?.requestCountryName }} </span>
|
||||
({{ authRequestResponse?.requestIpAddress }})
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<b>{{ "time" | i18n }}</b>
|
||||
|
||||
@@ -16,10 +16,10 @@ import {
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
block
|
||||
buttonType="secondary"
|
||||
class="tw-mt-4"
|
||||
(click)="startStandardAuthRequestLogin()"
|
||||
(click)="startStandardAuthRequestLogin(true)"
|
||||
>
|
||||
{{ "resendNotification" | i18n }}
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
@@ -17,7 +15,6 @@ import {
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
||||
@@ -25,9 +22,13 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
||||
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -40,6 +41,7 @@ import { ButtonModule, LinkModule, ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { AuthRequestApiService } from "../../common/abstractions/auth-request-api.service";
|
||||
import { LoginViaAuthRequestCacheService } from "../../common/services/auth-request/default-login-via-auth-request-cache.service";
|
||||
|
||||
enum Flow {
|
||||
StandardAuthRequest, // when user clicks "Login with device" from /login or "Approve from your other device" from /login-initiated
|
||||
@@ -57,23 +59,26 @@ const matchOptions: IsActiveMatchOptions = {
|
||||
standalone: true,
|
||||
templateUrl: "./login-via-auth-request.component.html",
|
||||
imports: [ButtonModule, CommonModule, JslibModule, LinkModule, RouterModule],
|
||||
providers: [{ provide: LoginViaAuthRequestCacheService }],
|
||||
})
|
||||
export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
private authRequest: AuthRequest;
|
||||
private authRequestKeyPair: { publicKey: Uint8Array; privateKey: Uint8Array };
|
||||
private authStatus: AuthenticationStatus;
|
||||
private authRequest: AuthRequest | undefined = undefined;
|
||||
private authRequestKeyPair:
|
||||
| { publicKey: Uint8Array | undefined; privateKey: Uint8Array | undefined }
|
||||
| undefined = undefined;
|
||||
private authStatus: AuthenticationStatus | undefined = undefined;
|
||||
private showResendNotificationTimeoutSeconds = 12;
|
||||
|
||||
protected backToRoute = "/login";
|
||||
protected clientType: ClientType;
|
||||
protected ClientType = ClientType;
|
||||
protected email: string;
|
||||
protected fingerprintPhrase: string;
|
||||
protected email: string | undefined = undefined;
|
||||
protected fingerprintPhrase: string | undefined = undefined;
|
||||
protected showResendNotification = false;
|
||||
protected Flow = Flow;
|
||||
protected flow = Flow.StandardAuthRequest;
|
||||
protected webVaultUrl: string;
|
||||
protected deviceManagementUrl: string;
|
||||
protected webVaultUrl: string | undefined = undefined;
|
||||
protected deviceManagementUrl: string | undefined;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
@@ -95,6 +100,8 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private validationService: ValidationService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
private loginViaAuthRequestCacheService: LoginViaAuthRequestCacheService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
|
||||
@@ -124,6 +131,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
async ngOnInit(): Promise<void> {
|
||||
// Get the authStatus early because we use it in both flows
|
||||
this.authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
||||
await this.loginViaAuthRequestCacheService.init();
|
||||
|
||||
const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked;
|
||||
|
||||
@@ -133,7 +141,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
|
||||
/**
|
||||
* The LoginViaAuthRequestComponent handles both the `login-with-device` and
|
||||
* the `admin-approval-requested` routes. Therefore we check the route to determine
|
||||
* the `admin-approval-requested` routes. Therefore, we check the route to determine
|
||||
* which flow to initialize.
|
||||
*/
|
||||
if (this.router.isActive("admin-approval-requested", matchOptions)) {
|
||||
@@ -159,7 +167,14 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
|
||||
// We only allow a single admin approval request to be active at a time
|
||||
// so we must check state to see if we have an existing one or not
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
if (!userId) {
|
||||
this.logService.error(
|
||||
"Not able to get a user id from the account service active account observable.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingAdminAuthRequest = await this.authRequestService.getAdminAuthRequest(userId);
|
||||
|
||||
if (existingAdminAuthRequest) {
|
||||
@@ -172,7 +187,9 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
private async initStandardAuthRequestFlow(): Promise<void> {
|
||||
this.flow = Flow.StandardAuthRequest;
|
||||
|
||||
this.email = await firstValueFrom(this.loginEmailService.loginEmail$);
|
||||
this.email = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
|
||||
if (!this.email) {
|
||||
await this.handleMissingEmail();
|
||||
@@ -185,7 +202,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
private async handleMissingEmail(): Promise<void> {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("userEmailMissing"),
|
||||
});
|
||||
|
||||
@@ -194,21 +210,41 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
|
||||
async ngOnDestroy(): Promise<void> {
|
||||
await this.anonymousHubService.stopHubConnection();
|
||||
|
||||
this.loginViaAuthRequestCacheService.clearCacheLoginView();
|
||||
}
|
||||
|
||||
private async startAdminAuthRequestLogin(): Promise<void> {
|
||||
try {
|
||||
await this.buildAuthRequest(AuthRequestType.AdminApproval);
|
||||
|
||||
if (!this.authRequest) {
|
||||
this.logService.error("Auth request failed to build.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.authRequestKeyPair) {
|
||||
this.logService.error("Key pairs failed to initialize from buildAuthRequest.");
|
||||
return;
|
||||
}
|
||||
|
||||
const authRequestResponse = await this.authRequestApiService.postAdminAuthRequest(
|
||||
this.authRequest,
|
||||
this.authRequest as AuthRequest,
|
||||
);
|
||||
const adminAuthReqStorable = new AdminAuthRequestStorable({
|
||||
id: authRequestResponse.id,
|
||||
privateKey: this.authRequestKeyPair.privateKey,
|
||||
});
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
if (!userId) {
|
||||
this.logService.error(
|
||||
"Not able to get a user id from the account service active account observable.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.authRequestService.setAdminAuthRequest(adminAuthReqStorable, userId);
|
||||
|
||||
if (authRequestResponse.id) {
|
||||
@@ -219,21 +255,104 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
protected async startStandardAuthRequestLogin(): Promise<void> {
|
||||
protected async startStandardAuthRequestLogin(
|
||||
clearCachedRequest: boolean = false,
|
||||
): Promise<void> {
|
||||
this.showResendNotification = false;
|
||||
|
||||
try {
|
||||
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
|
||||
|
||||
const authRequestResponse = await this.authRequestApiService.postAuthRequest(
|
||||
this.authRequest,
|
||||
);
|
||||
|
||||
if (authRequestResponse.id) {
|
||||
await this.anonymousHubService.createHubConnection(authRequestResponse.id);
|
||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM9112_DeviceApprovalPersistence)) {
|
||||
// Used for manually refreshing the auth request when clicking the resend auth request
|
||||
// on the ui.
|
||||
if (clearCachedRequest) {
|
||||
this.loginViaAuthRequestCacheService.clearCacheLoginView();
|
||||
}
|
||||
|
||||
try {
|
||||
const loginAuthRequestView: LoginViaAuthRequestView | null =
|
||||
this.loginViaAuthRequestCacheService.getCachedLoginViaAuthRequestView();
|
||||
|
||||
if (!loginAuthRequestView) {
|
||||
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
|
||||
|
||||
// I tried several ways to get the IDE/linter to play nice with checking for null values
|
||||
// in less code / more efficiently, but it struggles to identify code paths that
|
||||
// are more complicated than this.
|
||||
if (!this.authRequest) {
|
||||
this.logService.error("AuthRequest failed to initialize from buildAuthRequest.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.fingerprintPhrase) {
|
||||
this.logService.error("FingerprintPhrase failed to initialize from buildAuthRequest.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.authRequestKeyPair) {
|
||||
this.logService.error("KeyPair failed to initialize from buildAuthRequest.");
|
||||
return;
|
||||
}
|
||||
|
||||
const authRequestResponse: AuthRequestResponse =
|
||||
await this.authRequestApiService.postAuthRequest(this.authRequest);
|
||||
|
||||
this.loginViaAuthRequestCacheService.cacheLoginView(
|
||||
this.authRequest,
|
||||
authRequestResponse,
|
||||
this.fingerprintPhrase,
|
||||
this.authRequestKeyPair,
|
||||
);
|
||||
|
||||
if (authRequestResponse.id) {
|
||||
await this.anonymousHubService.createHubConnection(authRequestResponse.id);
|
||||
}
|
||||
} else {
|
||||
// Grab the cached information and store it back in component state.
|
||||
// We don't need the public key for handling the authentication request because
|
||||
// the verifyAndHandleApprovedAuthReq function will receive the public key back
|
||||
// from the looked up auth request and all we need is to make sure that
|
||||
// we can use the cached private key that is associated with it.
|
||||
this.authRequest = loginAuthRequestView.authRequest;
|
||||
this.fingerprintPhrase = loginAuthRequestView.fingerprintPhrase;
|
||||
this.authRequestKeyPair = {
|
||||
privateKey: loginAuthRequestView.privateKey
|
||||
? Utils.fromB64ToArray(loginAuthRequestView.privateKey)
|
||||
: undefined,
|
||||
publicKey: undefined,
|
||||
};
|
||||
|
||||
if (!loginAuthRequestView.authRequestResponse) {
|
||||
this.logService.error("No cached auth request response.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (loginAuthRequestView.authRequestResponse.id) {
|
||||
await this.anonymousHubService.createHubConnection(
|
||||
loginAuthRequestView.authRequestResponse.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
|
||||
|
||||
if (!this.authRequest) {
|
||||
this.logService.error("No auth request found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const authRequestResponse = await this.authRequestApiService.postAuthRequest(
|
||||
this.authRequest,
|
||||
);
|
||||
|
||||
if (authRequestResponse.id) {
|
||||
await this.anonymousHubService.createHubConnection(authRequestResponse.id);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -250,12 +369,23 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
|
||||
if (!this.authRequestKeyPair.publicKey) {
|
||||
this.logService.error("AuthRequest public key not set to value in building auth request.");
|
||||
return;
|
||||
}
|
||||
|
||||
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey);
|
||||
const accessCode = await this.passwordGenerationService.generatePassword({
|
||||
type: "password",
|
||||
length: 25,
|
||||
});
|
||||
|
||||
if (!this.email) {
|
||||
this.logService.error("Email not defined when building auth request.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||
this.email,
|
||||
this.authRequestKeyPair.publicKey,
|
||||
@@ -288,6 +418,8 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
|
||||
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
||||
}
|
||||
this.logService.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Request doesn't exist anymore
|
||||
@@ -300,6 +432,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey(
|
||||
adminAuthRequestStorable.privateKey,
|
||||
);
|
||||
|
||||
if (!this.email) {
|
||||
this.logService.error("Email not defined when handling an existing an admin auth request.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||
this.email,
|
||||
derivedPublicKeyArrayBuffer,
|
||||
@@ -319,9 +457,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
// Request still pending response from admin
|
||||
// set keypair and create hub connection so that any approvals will be received via push notification
|
||||
this.authRequestKeyPair = { privateKey: adminAuthRequestStorable.privateKey, publicKey: null };
|
||||
// Request still pending response from admin set keypair and create hub connection
|
||||
// so that any approvals will be received via push notification
|
||||
this.authRequestKeyPair = {
|
||||
privateKey: adminAuthRequestStorable.privateKey,
|
||||
publicKey: undefined,
|
||||
};
|
||||
await this.anonymousHubService.createHubConnection(adminAuthRequestStorable.id);
|
||||
}
|
||||
|
||||
@@ -382,7 +523,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
* | Standard Flow 1 | unauthed | "Login with device" [/login] | /login-with-device | yes |
|
||||
* | Standard Flow 2 | unauthed | "Login with device" [/login] | /login-with-device | no |
|
||||
* | Standard Flow 3 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | yes |
|
||||
* | Standard Flow 4 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | no | |
|
||||
* | Standard Flow 4 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | no |
|
||||
* | Admin Flow | authed | "Request admin approval" [/login-initiated] | /admin-approval-requested | NA - admin requests always send encrypted userKey |
|
||||
* |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
* * Note 1: The phrase "in memory" here is important. It is possible for a user to have a master password for their account, but not have a masterKey IN MEMORY for
|
||||
@@ -403,6 +544,11 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
await this.handleAuthenticatedFlows(authRequestResponse);
|
||||
}
|
||||
} else {
|
||||
if (!this.authRequest) {
|
||||
this.logService.error("No auth request defined when handling approved auth request.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the auth request from the server
|
||||
// User is unauthenticated, therefore the endpoint requires an access code for user verification.
|
||||
const authRequestResponse = await this.authRequestApiService.getAuthResponse(
|
||||
@@ -423,11 +569,26 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.logService.error(error);
|
||||
} finally {
|
||||
// Manually clean out the cache to make sure sensitive
|
||||
// data does not persist longer than it needs to.
|
||||
this.loginViaAuthRequestCacheService.clearCacheLoginView();
|
||||
}
|
||||
}
|
||||
|
||||
private async handleAuthenticatedFlows(authRequestResponse: AuthRequestResponse) {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
if (!userId) {
|
||||
this.logService.error(
|
||||
"Not able to get a user id from the account service active account observable.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.authRequestKeyPair || !this.authRequestKeyPair.privateKey) {
|
||||
this.logService.error("No private key set when handling the authenticated flows.");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.decryptViaApprovedAuthRequest(
|
||||
authRequestResponse,
|
||||
@@ -445,6 +606,11 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
authRequestResponse,
|
||||
);
|
||||
|
||||
if (!authRequestLoginCredentials) {
|
||||
this.logService.error("Didn't set up auth request login credentials properly.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: keys are set by AuthRequestLoginStrategy success handling
|
||||
const authResult = await this.loginStrategyService.logIn(authRequestLoginCredentials);
|
||||
|
||||
@@ -463,7 +629,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
* - If `masterPasswordHash` has a value, we receive the `key` as an authRequestPublicKey(masterKey) [plus we have authRequestPublicKey(masterPasswordHash)]
|
||||
* - If `masterPasswordHash` does not have a value, we receive the `key` as an authRequestPublicKey(userKey)
|
||||
*/
|
||||
|
||||
if (authRequestResponse.masterPasswordHash) {
|
||||
// ...in Standard Auth Request Flow 3
|
||||
await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash(
|
||||
@@ -486,13 +651,17 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("loginApproved"),
|
||||
});
|
||||
|
||||
// Now that we have a decrypted user key in memory, we can check if we
|
||||
// need to establish trust on the current device
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
if (!activeAccount) {
|
||||
this.logService.error("No active account defined from the account service.");
|
||||
return;
|
||||
}
|
||||
|
||||
await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id);
|
||||
|
||||
await this.handleSuccessfulLoginNavigation(userId);
|
||||
@@ -508,7 +677,24 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
private async buildAuthRequestLoginCredentials(
|
||||
requestId: string,
|
||||
authRequestResponse: AuthRequestResponse,
|
||||
): Promise<AuthRequestLoginCredentials> {
|
||||
): Promise<AuthRequestLoginCredentials | undefined> {
|
||||
if (!this.authRequestKeyPair || !this.authRequestKeyPair.privateKey) {
|
||||
this.logService.error("No private key set when building auth request login credentials.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.email) {
|
||||
this.logService.error("Email not defined.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.authRequest) {
|
||||
this.logService.error(
|
||||
"AuthRequest not defined when building auth request login credentials.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* See verifyAndHandleApprovedAuthReq() for flow details.
|
||||
*
|
||||
@@ -516,7 +702,6 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
* - If `masterPasswordHash` has a value, we receive the `key` as an authRequestPublicKey(masterKey) [plus we have authRequestPublicKey(masterPasswordHash)]
|
||||
* - If `masterPasswordHash` does not have a value, we receive the `key` as an authRequestPublicKey(userKey)
|
||||
*/
|
||||
|
||||
if (authRequestResponse.masterPasswordHash) {
|
||||
// ...in Standard Auth Request Flow 1
|
||||
const { masterKey, masterKeyHash } =
|
||||
|
||||
@@ -2,7 +2,7 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router, RouterModule } from "@angular/router";
|
||||
import { firstValueFrom, Subject, take, takeUntil, tap } from "rxjs";
|
||||
import { firstValueFrom, Subject, take, takeUntil } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -19,11 +19,9 @@ import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstraction
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -121,7 +119,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private logService: LogService,
|
||||
private validationService: ValidationService,
|
||||
private configService: ConfigService,
|
||||
private loginSuccessHandlerService: LoginSuccessHandlerService,
|
||||
) {
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
@@ -131,9 +128,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
// Add popstate listener to listen for browser back button clicks
|
||||
window.addEventListener("popstate", this.handlePopState);
|
||||
|
||||
// TODO: remove this when the UnauthenticatedExtensionUIRefresh feature flag is removed.
|
||||
this.listenForUnauthUiRefreshFlagChanges();
|
||||
|
||||
await this.defaultOnInit();
|
||||
|
||||
if (this.clientType === ClientType.Desktop) {
|
||||
@@ -154,30 +148,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private listenForUnauthUiRefreshFlagChanges() {
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
|
||||
.pipe(
|
||||
tap(async (flag) => {
|
||||
// If the flag is turned OFF, we must force a reload to ensure the correct UI is shown
|
||||
if (!flag) {
|
||||
const qParams = await firstValueFrom(this.activatedRoute.queryParams);
|
||||
const uniqueQueryParams = {
|
||||
...qParams,
|
||||
// adding a unique timestamp to the query params to force a reload
|
||||
t: new Date().getTime().toString(), // Adding a unique timestamp as a query parameter
|
||||
};
|
||||
|
||||
await this.router.navigate(["/"], {
|
||||
queryParams: uniqueQueryParams,
|
||||
});
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
if (this.clientType === ClientType.Desktop) {
|
||||
if (this.loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY) {
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
LinkModule,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { LoginEmailServiceAbstraction } from "../../common/abstractions/login-email.service";
|
||||
@@ -60,7 +59,6 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
|
||||
private apiService: ApiService,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
private syncService: SyncService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
@@ -153,9 +151,17 @@ export class NewDeviceVerificationComponent implements OnInit, OnDestroy {
|
||||
await this.router.navigate(["/vault"]);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
const errorMessage =
|
||||
(e as any)?.response?.error_description ?? this.i18nService.t("errorOccurred");
|
||||
let errorMessage =
|
||||
((e as any)?.response?.error_description as string) ?? this.i18nService.t("errorOccurred");
|
||||
|
||||
if (errorMessage.includes("Invalid New Device OTP")) {
|
||||
errorMessage = this.i18nService.t("invalidVerificationCode");
|
||||
}
|
||||
|
||||
codeControl.setErrors({ serverError: { message: errorMessage } });
|
||||
// For enter key press scenarios, we have to manually mark the control as touched
|
||||
// to get the error message to display
|
||||
codeControl.markAsTouched();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -31,6 +32,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
let sut: DefaultSetPasswordJitService;
|
||||
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
@@ -42,6 +44,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
masterPasswordApiService = mock<MasterPasswordApiService>();
|
||||
keyService = mock<KeyService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
i18nService = mock<I18nService>();
|
||||
@@ -53,6 +56,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
|
||||
sut = new DefaultSetPasswordJitService(
|
||||
apiService,
|
||||
masterPasswordApiService,
|
||||
keyService,
|
||||
encryptService,
|
||||
i18nService,
|
||||
@@ -148,7 +152,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
|
||||
keyService.makeKeyPair.mockResolvedValue(keyPair);
|
||||
|
||||
apiService.setPassword.mockResolvedValue(undefined);
|
||||
masterPasswordApiService.setPassword.mockResolvedValue(undefined);
|
||||
masterPasswordService.setForceSetPasswordReason.mockResolvedValue(undefined);
|
||||
|
||||
userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true }));
|
||||
@@ -185,7 +189,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
await sut.setPassword(credentials);
|
||||
|
||||
// Assert
|
||||
expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
});
|
||||
|
||||
it("should set password successfully (given no user key)", async () => {
|
||||
@@ -196,7 +200,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
await sut.setPassword(credentials);
|
||||
|
||||
// Assert
|
||||
expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
});
|
||||
|
||||
it("should handle reset password auto enroll", async () => {
|
||||
@@ -210,7 +214,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
await sut.setPassword(credentials);
|
||||
|
||||
// Assert
|
||||
expect(apiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(organizationApiService.getKeys).toHaveBeenCalledWith(orgId);
|
||||
expect(encryptService.rsaEncrypt).toHaveBeenCalledWith(userKey.key, orgPublicKey);
|
||||
expect(
|
||||
|
||||
@@ -9,10 +9,11 @@ import {
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected keyService: KeyService,
|
||||
protected encryptService: EncryptService,
|
||||
protected i18nService: I18nService,
|
||||
@@ -77,7 +79,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
kdfConfig.iterations,
|
||||
);
|
||||
|
||||
await this.apiService.setPassword(request);
|
||||
await this.masterPasswordApiService.setPassword(request);
|
||||
|
||||
// Clear force set password reason to allow navigation back to vault.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
|
||||
export type SsoClientType = ClientType.Web | ClientType.Browser | ClientType.Desktop;
|
||||
export type SsoClientType =
|
||||
| ClientType.Web
|
||||
| ClientType.Browser
|
||||
| ClientType.Desktop
|
||||
| ClientType.Cli;
|
||||
|
||||
/**
|
||||
* Abstract class for SSO component services.
|
||||
|
||||
@@ -19,13 +19,13 @@ import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/
|
||||
import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response";
|
||||
import { VerifiedOrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
|
||||
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -199,7 +199,9 @@ export class SsoComponent implements OnInit {
|
||||
* @returns True if the value is a valid SSO client type, otherwise false
|
||||
*/
|
||||
private isValidSsoClientType(value: string): value is SsoClientType {
|
||||
return [ClientType.Web, ClientType.Browser, ClientType.Desktop].includes(value as ClientType);
|
||||
return [ClientType.Web, ClientType.Browser, ClientType.Desktop, ClientType.Cli].includes(
|
||||
value as ClientType,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -108,6 +108,8 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy {
|
||||
this.webAuthnResultEmitter.emit({ token });
|
||||
},
|
||||
(error: string) => {
|
||||
this.logService.error("WebAuthn error: ", error);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
|
||||
@@ -6,7 +6,6 @@ import { ActivatedRoute, convertToParamMap, Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
@@ -19,14 +18,14 @@ import {
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -227,20 +226,6 @@ describe("TwoFactorAuthComponent", () => {
|
||||
});
|
||||
};
|
||||
|
||||
const testForceResetOnSuccessfulLogin = (reasonString: string) => {
|
||||
it(`navigates to the component's defined forcePasswordResetRoute route when response.forcePasswordReset is ${reasonString}`, async () => {
|
||||
// Act
|
||||
await component.submit("testToken");
|
||||
|
||||
// expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
|
||||
expect(mockRouter.navigate).toHaveBeenCalledWith(["update-temp-password"], {
|
||||
queryParams: {
|
||||
identifier: component.orgSsoIdentifier,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe("Standard 2FA scenarios", () => {
|
||||
describe("submit", () => {
|
||||
const token = "testToken";
|
||||
@@ -312,26 +297,6 @@ describe("TwoFactorAuthComponent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Force Master Password Reset scenarios", () => {
|
||||
[
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
].forEach((forceResetPasswordReason) => {
|
||||
const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
|
||||
|
||||
beforeEach(() => {
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = forceResetPasswordReason;
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
testForceResetOnSuccessfulLogin(reasonString);
|
||||
});
|
||||
});
|
||||
|
||||
it("navigates to the component's defined success route (vault is default) when the login is successful", async () => {
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(new AuthResult());
|
||||
|
||||
@@ -408,29 +373,7 @@ describe("TwoFactorAuthComponent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is required", () => {
|
||||
[
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
].forEach((forceResetPasswordReason) => {
|
||||
const reasonString = ForceSetPasswordReason[forceResetPasswordReason];
|
||||
|
||||
beforeEach(() => {
|
||||
// use standard user with MP because this test is not concerned with password reset.
|
||||
selectedUserDecryptionOptions.next(
|
||||
mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice,
|
||||
);
|
||||
|
||||
const authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = forceResetPasswordReason;
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
testForceResetOnSuccessfulLogin(reasonString);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => {
|
||||
describe("Given Trusted Device Encryption is enabled and user doesn't need to set a MP", () => {
|
||||
let authResult;
|
||||
beforeEach(() => {
|
||||
selectedUserDecryptionOptions.next(
|
||||
@@ -438,7 +381,6 @@ describe("TwoFactorAuthComponent", () => {
|
||||
);
|
||||
|
||||
authResult = new AuthResult();
|
||||
authResult.forcePasswordReset = ForceSetPasswordReason.None;
|
||||
mockLoginStrategyService.logInTwoFactor.mockResolvedValue(authResult);
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
LoginSuccessHandlerService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
@@ -32,6 +31,7 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -188,8 +188,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
this.twoFactorAuthComponentService.shouldCheckForWebAuthnQueryParamResponse() &&
|
||||
webAuthnSupported
|
||||
) {
|
||||
const webAuthn2faResponse =
|
||||
this.activatedRoute.snapshot.queryParamMap.get("webAuthnResponse");
|
||||
const webAuthn2faResponse = this.activatedRoute.snapshot.paramMap.get("webAuthnResponse");
|
||||
if (webAuthn2faResponse) {
|
||||
this.selectedProviderType = TwoFactorProviderType.WebAuthn;
|
||||
return;
|
||||
@@ -396,11 +395,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
// note: this flow affects both TDE & standard users
|
||||
if (this.isForcePasswordResetRequired(authResult)) {
|
||||
return await this.handleForcePasswordReset(this.orgSsoIdentifier);
|
||||
}
|
||||
|
||||
const userDecryptionOpts = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
@@ -415,6 +409,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
const requireSetPassword =
|
||||
!userDecryptionOpts.hasMasterPassword && userDecryptionOpts.keyConnectorOption === undefined;
|
||||
|
||||
// New users without a master password must set a master password before advancing.
|
||||
if (requireSetPassword || authResult.resetMasterPassword) {
|
||||
// Change implies going no password -> password in this case
|
||||
return await this.handleChangePasswordRequired(this.orgSsoIdentifier);
|
||||
@@ -524,14 +519,6 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy {
|
||||
return forceResetReasons.includes(authResult.forcePasswordReset);
|
||||
}
|
||||
|
||||
private async handleForcePasswordReset(orgIdentifier: string | undefined) {
|
||||
await this.router.navigate(["update-temp-password"], {
|
||||
queryParams: {
|
||||
identifier: orgIdentifier,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
showContinueButton() {
|
||||
return (
|
||||
this.selectedProviderType != null &&
|
||||
|
||||
@@ -68,7 +68,6 @@ import { ActiveClientVerificationOption } from "./active-client-verification-opt
|
||||
CalloutModule,
|
||||
],
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class UserVerificationFormInputComponent implements ControlValueAccessor, OnInit, OnDestroy {
|
||||
@Input() verificationType: "server" | "client" = "server"; // server represents original behavior
|
||||
private _invalidSecret = false;
|
||||
|
||||
@@ -12,6 +12,12 @@ export abstract class AuthRequestServiceAbstraction {
|
||||
/** Emits an auth request id when an auth request has been approved. */
|
||||
authRequestPushNotification$: Observable<string>;
|
||||
|
||||
/**
|
||||
* Emits when a login has been approved by an admin. This emission is specifically for the
|
||||
* purpose of notifying the consuming component to display a toast informing the user.
|
||||
*/
|
||||
adminLoginApproved$: Observable<void>;
|
||||
|
||||
/**
|
||||
* Returns an admin auth request for the given user if it exists.
|
||||
* @param userId The user id.
|
||||
@@ -106,4 +112,13 @@ export abstract class AuthRequestServiceAbstraction {
|
||||
* @returns The dash-delimited fingerprint phrase.
|
||||
*/
|
||||
abstract getFingerprintPhrase(email: string, publicKey: Uint8Array): Promise<string>;
|
||||
|
||||
/**
|
||||
* Passes a value to the adminLoginApprovedSubject via next(), which causes the
|
||||
* adminLoginApproved$ observable to emit.
|
||||
*
|
||||
* The purpose is to notify consuming components (of adminLoginApproved$) to display
|
||||
* a toast informing the user that a login has been approved by an admin.
|
||||
*/
|
||||
abstract emitAdminLoginApproved(): void;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutSettingsService,
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
@@ -17,9 +17,9 @@ import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/id
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
|
||||
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutSettingsService,
|
||||
@@ -306,6 +306,31 @@ describe("LoginStrategy", () => {
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it("processes a forcePasswordReset response properly", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.forcePasswordReset = true;
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
const result = await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.userId = userId;
|
||||
expected.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset;
|
||||
expected.resetMasterPassword = false;
|
||||
expected.twoFactorProviders = {} as Partial<
|
||||
Record<TwoFactorProviderType, Record<string, string>>
|
||||
>;
|
||||
expected.captchaSiteKey = "";
|
||||
expected.twoFactorProviders = null;
|
||||
expect(result).toEqual(expected);
|
||||
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects login if CAPTCHA is required", async () => {
|
||||
// Sample CAPTCHA response
|
||||
const tokenResponse = new IdentityCaptchaResponse({
|
||||
|
||||
@@ -2,7 +2,6 @@ import { BehaviorSubject, filter, firstValueFrom, timeout, Observable } from "rx
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
@@ -21,6 +20,7 @@ import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/respons
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutSettingsService,
|
||||
@@ -271,17 +271,24 @@ export abstract class LoginStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
result.resetMasterPassword = response.resetMasterPassword;
|
||||
|
||||
// Convert boolean to enum
|
||||
if (response.forcePasswordReset) {
|
||||
result.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset;
|
||||
}
|
||||
|
||||
// Must come before setting keys, user key needs email to update additional keys
|
||||
// Must come before setting keys, user key needs email to update additional keys.
|
||||
const userId = await this.saveAccountInformation(response);
|
||||
result.userId = userId;
|
||||
|
||||
result.resetMasterPassword = response.resetMasterPassword;
|
||||
|
||||
// Convert boolean to enum and set the state for the master password service to
|
||||
// so we know when we reach the auth guard that we need to guide them properly to admin
|
||||
// password reset.
|
||||
if (response.forcePasswordReset) {
|
||||
result.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset;
|
||||
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
if (response.twoFactorToken != null) {
|
||||
// note: we can read email from access token b/c it was saved in saveAccountInformation
|
||||
const userEmail = await this.tokenService.getEmail();
|
||||
@@ -300,7 +307,9 @@ export abstract class LoginStrategy {
|
||||
|
||||
// The keys comes from different sources depending on the login strategy
|
||||
protected abstract setMasterKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||
|
||||
protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||
|
||||
protected abstract setPrivateKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||
|
||||
// Old accounts used master key for encryption. We are forcing migrations but only need to
|
||||
|
||||
@@ -10,9 +10,9 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutSettingsService,
|
||||
|
||||
@@ -2,17 +2,17 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutSettingsService,
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { HttpStatusCode } from "@bitwarden/common/enums";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -108,14 +107,6 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
const email = ssoAuthResult.email;
|
||||
const ssoEmail2FaSessionToken = ssoAuthResult.ssoEmail2FaSessionToken;
|
||||
|
||||
// Auth guard currently handles redirects for this.
|
||||
if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ssoAuthResult.forcePasswordReset,
|
||||
ssoAuthResult.userId,
|
||||
);
|
||||
}
|
||||
|
||||
this.cache.next({
|
||||
...this.cache.value,
|
||||
email,
|
||||
@@ -278,7 +269,8 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
// TODO: eventually we post and clean up DB as well once consumed on client
|
||||
await this.authRequestService.clearAdminAuthRequest(userId);
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("loginApproved"));
|
||||
// This notification will be picked up by the SsoComponent to handle displaying a toast to the user
|
||||
this.authRequestService.emitAdminLoginApproved();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutSettingsService,
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { firstValueFrom, BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutSettingsService,
|
||||
|
||||
@@ -53,9 +53,9 @@ export class AuthRequestLoginCredentials {
|
||||
public email: string,
|
||||
public accessCode: string,
|
||||
public authRequestId: string,
|
||||
public decryptedUserKey: UserKey,
|
||||
public decryptedMasterKey: MasterKey,
|
||||
public decryptedMasterKeyHash: string,
|
||||
public decryptedUserKey: UserKey | null,
|
||||
public decryptedMasterKey: MasterKey | null,
|
||||
public decryptedMasterKeyHash: string | null,
|
||||
public twoFactor?: TokenTwoFactorRequest,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { mock } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
@@ -5,11 +5,11 @@ import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
||||
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
@@ -43,6 +43,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
private authRequestPushNotificationSubject = new Subject<string>();
|
||||
authRequestPushNotification$: Observable<string>;
|
||||
|
||||
// Observable emission is used to trigger a toast in consuming components
|
||||
private adminLoginApprovedSubject = new Subject<void>();
|
||||
adminLoginApproved$: Observable<void>;
|
||||
|
||||
constructor(
|
||||
private appIdService: AppIdService,
|
||||
private accountService: AccountService,
|
||||
@@ -53,6 +57,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
private stateProvider: StateProvider,
|
||||
) {
|
||||
this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable();
|
||||
this.adminLoginApproved$ = this.adminLoginApprovedSubject.asObservable();
|
||||
}
|
||||
|
||||
async getAdminAuthRequest(userId: UserId): Promise<AdminAuthRequestStorable | null> {
|
||||
@@ -207,4 +212,8 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
async getFingerprintPhrase(email: string, publicKey: Uint8Array): Promise<string> {
|
||||
return (await this.keyService.getFingerprint(email.toLowerCase(), publicKey)).join("-");
|
||||
}
|
||||
|
||||
emitAdminLoginApproved(): void {
|
||||
this.adminLoginApprovedSubject.next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { signal } from "@angular/core";
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type";
|
||||
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { LoginViaAuthRequestCacheService } from "./default-login-via-auth-request-cache.service";
|
||||
|
||||
describe("LoginViaAuthRequestCache", () => {
|
||||
let service: LoginViaAuthRequestCacheService;
|
||||
let testBed: TestBed;
|
||||
|
||||
const cacheSignal = signal<LoginViaAuthRequestView | null>(null);
|
||||
const getCacheSignal = jest.fn().mockReturnValue(cacheSignal);
|
||||
const getFeatureFlag = jest.fn().mockResolvedValue(false);
|
||||
const cacheSetMock = jest.spyOn(cacheSignal, "set");
|
||||
|
||||
beforeEach(() => {
|
||||
getCacheSignal.mockClear();
|
||||
getFeatureFlag.mockClear();
|
||||
cacheSetMock.mockClear();
|
||||
|
||||
testBed = TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ViewCacheService, useValue: { signal: getCacheSignal } },
|
||||
{ provide: ConfigService, useValue: { getFeatureFlag } },
|
||||
LoginViaAuthRequestCacheService,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe("feature enabled", () => {
|
||||
beforeEach(() => {
|
||||
getFeatureFlag.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("`getCachedLoginViaAuthRequestView` returns the cached data", async () => {
|
||||
cacheSignal.set({ ...buildAuthenticMockAuthView() });
|
||||
service = testBed.inject(LoginViaAuthRequestCacheService);
|
||||
await service.init();
|
||||
|
||||
expect(service.getCachedLoginViaAuthRequestView()).toEqual({
|
||||
...buildAuthenticMockAuthView(),
|
||||
});
|
||||
});
|
||||
|
||||
it("updates the signal value", async () => {
|
||||
service = testBed.inject(LoginViaAuthRequestCacheService);
|
||||
await service.init();
|
||||
|
||||
const parameters = buildAuthenticMockAuthView();
|
||||
|
||||
service.cacheLoginView(
|
||||
parameters.authRequest,
|
||||
parameters.authRequestResponse,
|
||||
parameters.fingerprintPhrase,
|
||||
{ publicKey: new Uint8Array(), privateKey: new Uint8Array() },
|
||||
);
|
||||
|
||||
expect(cacheSignal.set).toHaveBeenCalledWith(parameters);
|
||||
});
|
||||
});
|
||||
|
||||
describe("feature disabled", () => {
|
||||
beforeEach(async () => {
|
||||
cacheSignal.set({ ...buildAuthenticMockAuthView() } as LoginViaAuthRequestView);
|
||||
getFeatureFlag.mockResolvedValue(false);
|
||||
cacheSetMock.mockClear();
|
||||
|
||||
service = testBed.inject(LoginViaAuthRequestCacheService);
|
||||
await service.init();
|
||||
});
|
||||
|
||||
it("`getCachedCipherView` returns null", () => {
|
||||
expect(service.getCachedLoginViaAuthRequestView()).toBeNull();
|
||||
});
|
||||
|
||||
it("does not update the signal value", () => {
|
||||
const params = buildAuthenticMockAuthView();
|
||||
|
||||
service.cacheLoginView(
|
||||
params.authRequest,
|
||||
params.authRequestResponse,
|
||||
params.fingerprintPhrase,
|
||||
{ publicKey: new Uint8Array(), privateKey: new Uint8Array() },
|
||||
);
|
||||
|
||||
expect(cacheSignal.set).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
const buildAuthenticMockAuthView = () => {
|
||||
return {
|
||||
fingerprintPhrase: "",
|
||||
privateKey: "",
|
||||
publicKey: "",
|
||||
authRequest: new AuthRequest(
|
||||
"test@gmail.com",
|
||||
"deviceIdentifier",
|
||||
"publicKey",
|
||||
AuthRequestType.Unlock,
|
||||
"accessCode",
|
||||
),
|
||||
authRequestResponse: new AuthRequestResponse({}),
|
||||
};
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { inject, Injectable, WritableSignal } from "@angular/core";
|
||||
|
||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
const LOGIN_VIA_AUTH_CACHE_KEY = "login-via-auth-request-form-cache";
|
||||
|
||||
/**
|
||||
* This is a cache service used for the login via auth request component.
|
||||
*
|
||||
* There is sensitive information stored temporarily here. Cache will be cleared
|
||||
* after 2 minutes.
|
||||
*/
|
||||
@Injectable()
|
||||
export class LoginViaAuthRequestCacheService {
|
||||
private viewCacheService: ViewCacheService = inject(ViewCacheService);
|
||||
private configService: ConfigService = inject(ConfigService);
|
||||
|
||||
/** True when the `PM9112_DeviceApproval` flag is enabled */
|
||||
private featureEnabled: boolean = false;
|
||||
|
||||
private defaultLoginViaAuthRequestCache: WritableSignal<LoginViaAuthRequestView | null> =
|
||||
this.viewCacheService.signal<LoginViaAuthRequestView | null>({
|
||||
key: LOGIN_VIA_AUTH_CACHE_KEY,
|
||||
initialValue: null,
|
||||
deserializer: LoginViaAuthRequestView.fromJSON,
|
||||
});
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Must be called once before interacting with the cached data, otherwise methods will be noop.
|
||||
*/
|
||||
async init() {
|
||||
this.featureEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM9112_DeviceApprovalPersistence,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cache with the new LoginView.
|
||||
*/
|
||||
cacheLoginView(
|
||||
authRequest: AuthRequest,
|
||||
authRequestResponse: AuthRequestResponse,
|
||||
fingerprintPhrase: string,
|
||||
keys: { privateKey: Uint8Array | undefined; publicKey: Uint8Array | undefined },
|
||||
): void {
|
||||
if (!this.featureEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When the keys get stored they should be converted to a B64 string to ensure
|
||||
// data can be properly formed when json-ified. If not done, they are not stored properly and
|
||||
// will not be parsable by the cryptography library after coming out of storage.
|
||||
this.defaultLoginViaAuthRequestCache.set({
|
||||
authRequest,
|
||||
authRequestResponse,
|
||||
fingerprintPhrase,
|
||||
privateKey: keys.privateKey ? Utils.fromBufferToB64(keys.privateKey.buffer) : undefined,
|
||||
publicKey: keys.publicKey ? Utils.fromBufferToB64(keys.publicKey.buffer) : undefined,
|
||||
} as LoginViaAuthRequestView);
|
||||
}
|
||||
|
||||
clearCacheLoginView(): void {
|
||||
if (!this.featureEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.defaultLoginViaAuthRequestCache.set(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cached LoginViaAuthRequestView when available.
|
||||
*/
|
||||
getCachedLoginViaAuthRequestView(): LoginViaAuthRequestView | null {
|
||||
if (!this.featureEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.defaultLoginViaAuthRequestCache();
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@ import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||
@@ -13,9 +11,11 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { PreloginResponse } from "@bitwarden/common/auth/models/response/prelogin.response";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import {
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutSettingsService,
|
||||
|
||||
@@ -12,9 +12,6 @@ import {
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||
@@ -22,6 +19,9 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/key-management/master-password/services/fake-master-password.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
ProviderUserUserDetailsResponse,
|
||||
} from "../admin-console/models/response/provider/provider-user.response";
|
||||
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
||||
import { AuthRequest } from "../auth/models/request/auth.request";
|
||||
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
|
||||
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
|
||||
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
|
||||
@@ -47,19 +46,12 @@ import { PasswordTokenRequest } from "../auth/models/request/identity-token/pass
|
||||
import { SsoTokenRequest } from "../auth/models/request/identity-token/sso-token.request";
|
||||
import { UserApiTokenRequest } from "../auth/models/request/identity-token/user-api-token.request";
|
||||
import { WebAuthnLoginTokenRequest } from "../auth/models/request/identity-token/webauthn-login-token.request";
|
||||
import { KeyConnectorUserKeyRequest } from "../auth/models/request/key-connector-user-key.request";
|
||||
import { PasswordHintRequest } from "../auth/models/request/password-hint.request";
|
||||
import { PasswordRequest } from "../auth/models/request/password.request";
|
||||
import { PasswordlessAuthRequest } from "../auth/models/request/passwordless-auth.request";
|
||||
import { SecretVerificationRequest } from "../auth/models/request/secret-verification.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../auth/models/request/set-key-connector-key.request";
|
||||
import { SetPasswordRequest } from "../auth/models/request/set-password.request";
|
||||
import { TwoFactorEmailRequest } from "../auth/models/request/two-factor-email.request";
|
||||
import { TwoFactorProviderRequest } from "../auth/models/request/two-factor-provider.request";
|
||||
import { TwoFactorRecoveryRequest } from "../auth/models/request/two-factor-recovery.request";
|
||||
import { UpdateProfileRequest } from "../auth/models/request/update-profile.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "../auth/models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "../auth/models/request/update-temp-password.request";
|
||||
import { UpdateTwoFactorAuthenticatorRequest } from "../auth/models/request/update-two-factor-authenticator.request";
|
||||
import { UpdateTwoFactorDuoRequest } from "../auth/models/request/update-two-factor-duo.request";
|
||||
import { UpdateTwoFactorEmailRequest } from "../auth/models/request/update-two-factor-email.request";
|
||||
@@ -96,6 +88,8 @@ import { PaymentResponse } from "../billing/models/response/payment.response";
|
||||
import { PlanResponse } from "../billing/models/response/plan.response";
|
||||
import { SubscriptionResponse } from "../billing/models/response/subscription.response";
|
||||
import { TaxInfoResponse } from "../billing/models/response/tax-info.response";
|
||||
import { KeyConnectorUserKeyRequest } from "../key-management/key-connector/models/key-connector-user-key.request";
|
||||
import { SetKeyConnectorKeyRequest } from "../key-management/key-connector/models/set-key-connector-key.request";
|
||||
import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
|
||||
import { EventRequest } from "../models/request/event.request";
|
||||
import { KdfRequest } from "../models/request/kdf.request";
|
||||
@@ -169,8 +163,6 @@ export abstract class ApiService {
|
||||
postPrelogin: (request: PreloginRequest) => Promise<PreloginResponse>;
|
||||
postEmailToken: (request: EmailTokenRequest) => Promise<any>;
|
||||
postEmail: (request: EmailRequest) => Promise<any>;
|
||||
postPassword: (request: PasswordRequest) => Promise<any>;
|
||||
setPassword: (request: SetPasswordRequest) => Promise<any>;
|
||||
postSetKeyConnectorKey: (request: SetKeyConnectorKeyRequest) => Promise<any>;
|
||||
postSecurityStamp: (request: SecretVerificationRequest) => Promise<any>;
|
||||
getAccountRevisionDate: () => Promise<number>;
|
||||
@@ -189,13 +181,8 @@ export abstract class ApiService {
|
||||
postAccountKdf: (request: KdfRequest) => Promise<any>;
|
||||
postUserApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
|
||||
postUserRotateApiKey: (id: string, request: SecretVerificationRequest) => Promise<ApiKeyResponse>;
|
||||
putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise<any>;
|
||||
putUpdateTdeOffboardingPassword: (request: UpdateTdeOffboardingPasswordRequest) => Promise<any>;
|
||||
postConvertToKeyConnector: () => Promise<void>;
|
||||
//passwordless
|
||||
postAuthRequest: (request: AuthRequest) => Promise<AuthRequestResponse>;
|
||||
postAdminAuthRequest: (request: AuthRequest) => Promise<AuthRequestResponse>;
|
||||
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
|
||||
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
|
||||
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
|
||||
getAuthRequests: () => Promise<ListResponse<AuthRequestResponse>>;
|
||||
@@ -356,7 +343,6 @@ export abstract class ApiService {
|
||||
organizationId: string,
|
||||
request: TwoFactorProviderRequest,
|
||||
) => Promise<TwoFactorProviderResponse>;
|
||||
postTwoFactorRecover: (request: TwoFactorRecoveryRequest) => Promise<any>;
|
||||
postTwoFactorEmailSetup: (request: TwoFactorEmailRequest) => Promise<any>;
|
||||
postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise<any>;
|
||||
getDeviceVerificationSettings: () => Promise<DeviceVerificationResponse>;
|
||||
|
||||
@@ -3,16 +3,21 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { SendView } from "../tools/send/models/view/send.view";
|
||||
import { IndexedEntityId } from "../types/guid";
|
||||
import { IndexedEntityId, UserId } from "../types/guid";
|
||||
import { CipherView } from "../vault/models/view/cipher.view";
|
||||
|
||||
export abstract class SearchService {
|
||||
indexedEntityId$: Observable<IndexedEntityId | null>;
|
||||
indexedEntityId$: (userId: UserId) => Observable<IndexedEntityId | null>;
|
||||
|
||||
clearIndex: () => Promise<void>;
|
||||
isSearchable: (query: string) => Promise<boolean>;
|
||||
indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => Promise<void>;
|
||||
clearIndex: (userId: UserId) => Promise<void>;
|
||||
isSearchable: (userId: UserId, query: string) => Promise<boolean>;
|
||||
indexCiphers: (
|
||||
userId: UserId,
|
||||
ciphersToIndex: CipherView[],
|
||||
indexedEntityGuid?: string,
|
||||
) => Promise<void>;
|
||||
searchCiphers: (
|
||||
userId: UserId,
|
||||
query: string,
|
||||
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],
|
||||
ciphers?: CipherView[],
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { PasswordRequest } from "../models/request/password.request";
|
||||
import { SetPasswordRequest } from "../models/request/set-password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "../models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "../models/request/update-temp-password.request";
|
||||
|
||||
export abstract class MasterPasswordApiService {
|
||||
/**
|
||||
* POSTs a SetPasswordRequest to "/accounts/set-password"
|
||||
*/
|
||||
abstract setPassword: (request: SetPasswordRequest) => Promise<any>;
|
||||
|
||||
/**
|
||||
* POSTs a PasswordRequest to "/accounts/password"
|
||||
*/
|
||||
abstract postPassword: (request: PasswordRequest) => Promise<any>;
|
||||
|
||||
/**
|
||||
* PUTs an UpdateTempPasswordRequest to "/accounts/update-temp-password"
|
||||
*/
|
||||
abstract putUpdateTempPassword: (request: UpdateTempPasswordRequest) => Promise<any>;
|
||||
|
||||
/**
|
||||
* PUTs an UpdateTdeOffboardingPasswordRequest to "/accounts/update-tde-offboarding-password"
|
||||
*/
|
||||
abstract putUpdateTdeOffboardingPassword: (
|
||||
request: UpdateTdeOffboardingPasswordRequest,
|
||||
) => Promise<any>;
|
||||
}
|
||||
@@ -14,7 +14,6 @@ export abstract class TokenRequest {
|
||||
this.device = device != null ? device : null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
alterIdentityTokenHeaders(headers: Headers) {
|
||||
// Implemented in subclass if required
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { SecretVerificationRequest } from "./secret-verification.request";
|
||||
|
||||
export class TwoFactorRecoveryRequest extends SecretVerificationRequest {
|
||||
recoveryCode: string;
|
||||
email: string;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export class AuthRequestResponse extends BaseResponse {
|
||||
requestDeviceTypeValue: DeviceType;
|
||||
requestDeviceIdentifier: string;
|
||||
requestIpAddress: string;
|
||||
requestCountryName: string;
|
||||
key: string; // could be either an encrypted MasterKey or an encrypted UserKey
|
||||
masterPasswordHash: string; // if hash is present, the `key` above is an encrypted MasterKey (else `key` is an encrypted UserKey)
|
||||
creationDate: string;
|
||||
@@ -26,6 +27,7 @@ export class AuthRequestResponse extends BaseResponse {
|
||||
this.requestDeviceTypeValue = this.getResponseProperty("RequestDeviceTypeValue");
|
||||
this.requestDeviceIdentifier = this.getResponseProperty("RequestDeviceIdentifier");
|
||||
this.requestIpAddress = this.getResponseProperty("RequestIpAddress");
|
||||
this.requestCountryName = this.getResponseProperty("RequestCountryName");
|
||||
this.key = this.getResponseProperty("Key");
|
||||
this.masterPasswordHash = this.getResponseProperty("MasterPasswordHash");
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { View } from "@bitwarden/common/models/view/view";
|
||||
|
||||
export class LoginViaAuthRequestView implements View {
|
||||
authRequest: AuthRequest | undefined = undefined;
|
||||
authRequestResponse: AuthRequestResponse | undefined = undefined;
|
||||
fingerprintPhrase: string | undefined = undefined;
|
||||
privateKey: string | undefined = undefined;
|
||||
publicKey: string | undefined = undefined;
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<LoginViaAuthRequestView>>): LoginViaAuthRequestView {
|
||||
return Object.assign(new LoginViaAuthRequestView(), obj);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
||||
import { MasterPasswordApiService as MasterPasswordApiServiceAbstraction } from "../../abstractions/master-password-api.service.abstraction";
|
||||
import { PasswordRequest } from "../../models/request/password.request";
|
||||
import { SetPasswordRequest } from "../../models/request/set-password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "../../models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "../../models/request/update-temp-password.request";
|
||||
|
||||
export class MasterPasswordApiService implements MasterPasswordApiServiceAbstraction {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async setPassword(request: SetPasswordRequest): Promise<any> {
|
||||
try {
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
"/accounts/set-password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async postPassword(request: PasswordRequest): Promise<any> {
|
||||
try {
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
"/accounts/password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async putUpdateTempPassword(request: UpdateTempPasswordRequest): Promise<any> {
|
||||
try {
|
||||
const response = await this.apiService.send(
|
||||
"PUT",
|
||||
"/accounts/update-temp-password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async putUpdateTdeOffboardingPassword(
|
||||
request: UpdateTdeOffboardingPasswordRequest,
|
||||
): Promise<any> {
|
||||
try {
|
||||
const response = await this.apiService.send(
|
||||
"PUT",
|
||||
"/accounts/update-tde-offboarding-password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
this.logService.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { KdfType } from "@bitwarden/key-management";
|
||||
|
||||
import { PasswordRequest } from "../../models/request/password.request";
|
||||
import { SetPasswordRequest } from "../../models/request/set-password.request";
|
||||
import { UpdateTdeOffboardingPasswordRequest } from "../../models/request/update-tde-offboarding-password.request";
|
||||
import { UpdateTempPasswordRequest } from "../../models/request/update-temp-password.request";
|
||||
|
||||
import { MasterPasswordApiService } from "./master-password-api.service.implementation";
|
||||
|
||||
describe("MasterPasswordApiService", () => {
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
|
||||
let sut: MasterPasswordApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
logService = mock<LogService>();
|
||||
|
||||
sut = new MasterPasswordApiService(apiService, logService);
|
||||
});
|
||||
|
||||
it("should instantiate", () => {
|
||||
expect(sut).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("setPassword", () => {
|
||||
it("should call apiService.send with the correct parameters", async () => {
|
||||
// Arrange
|
||||
const request = new SetPasswordRequest(
|
||||
"masterPasswordHash",
|
||||
"key",
|
||||
"masterPasswordHint",
|
||||
"orgIdentifier",
|
||||
{
|
||||
publicKey: "publicKey",
|
||||
encryptedPrivateKey: "encryptedPrivateKey",
|
||||
},
|
||||
KdfType.PBKDF2_SHA256,
|
||||
600_000,
|
||||
);
|
||||
|
||||
// Act
|
||||
await sut.setPassword(request);
|
||||
|
||||
// Assert
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
"/accounts/set-password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("postPassword", () => {
|
||||
it("should call apiService.send with the correct parameters", async () => {
|
||||
// Arrange
|
||||
const request = {
|
||||
newMasterPasswordHash: "newMasterPasswordHash",
|
||||
masterPasswordHint: "masterPasswordHint",
|
||||
key: "key",
|
||||
masterPasswordHash: "masterPasswordHash",
|
||||
} as PasswordRequest;
|
||||
|
||||
// Act
|
||||
await sut.postPassword(request);
|
||||
|
||||
// Assert
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"POST",
|
||||
"/accounts/password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("putUpdateTempPassword", () => {
|
||||
it("should call apiService.send with the correct parameters", async () => {
|
||||
// Arrange
|
||||
const request = {
|
||||
masterPasswordHint: "masterPasswordHint",
|
||||
newMasterPasswordHash: "newMasterPasswordHash",
|
||||
key: "key",
|
||||
} as UpdateTempPasswordRequest;
|
||||
|
||||
// Act
|
||||
await sut.putUpdateTempPassword(request);
|
||||
|
||||
// Assert
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"PUT",
|
||||
"/accounts/update-temp-password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("putUpdateTdeOffboardingPassword", () => {
|
||||
it("should call apiService.send with the correct parameters", async () => {
|
||||
// Arrange
|
||||
const request = {
|
||||
masterPasswordHint: "masterPasswordHint",
|
||||
newMasterPasswordHash: "newMasterPasswordHash",
|
||||
key: "key",
|
||||
} as UpdateTdeOffboardingPasswordRequest;
|
||||
|
||||
// Act
|
||||
await sut.putUpdateTdeOffboardingPassword(request);
|
||||
|
||||
// Assert
|
||||
expect(apiService.send).toHaveBeenCalledWith(
|
||||
"PUT",
|
||||
"/accounts/update-tde-offboarding-password",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,13 +16,13 @@ import {
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../../key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "../../../key-management/vault-timeout";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { HashPurpose } from "../../../platform/enums";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey } from "../../../types/key";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { VerificationType } from "../../enums/verification-type";
|
||||
import { MasterPasswordPolicyResponse } from "../../models/response/master-password-policy.response";
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../../key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { HashPurpose } from "../../../platform/enums";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { AccountService } from "../../abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "../../enums/verification-type";
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
|
||||
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
|
||||
|
||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
|
||||
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
|
||||
@@ -50,6 +52,8 @@ export abstract class BillingApiServiceAbstraction {
|
||||
|
||||
getProviderSubscription: (providerId: string) => Promise<ProviderSubscriptionResponse>;
|
||||
|
||||
getProviderTaxInformation: (providerId: string) => Promise<TaxInfoResponse>;
|
||||
|
||||
updateOrganizationPaymentMethod: (
|
||||
organizationId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
@@ -66,6 +70,11 @@ export abstract class BillingApiServiceAbstraction {
|
||||
request: UpdateClientOrganizationRequest,
|
||||
) => Promise<any>;
|
||||
|
||||
updateProviderPaymentMethod: (
|
||||
providerId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
) => Promise<void>;
|
||||
|
||||
updateProviderTaxInformation: (
|
||||
providerId: string,
|
||||
request: ExpandedTaxInfoUpdateRequest,
|
||||
@@ -76,6 +85,11 @@ export abstract class BillingApiServiceAbstraction {
|
||||
request: VerifyBankAccountRequest,
|
||||
) => Promise<void>;
|
||||
|
||||
verifyProviderBankAccount: (
|
||||
providerId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
) => Promise<void>;
|
||||
|
||||
restartSubscription: (
|
||||
organizationId: string,
|
||||
request: OrganizationCreateRequest,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response";
|
||||
|
||||
import { ProviderType } from "../../../admin-console/enums";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { PlanType, ProductTierType } from "../../enums";
|
||||
@@ -16,6 +18,7 @@ export class ProviderSubscriptionResponse extends BaseResponse {
|
||||
cancelAt?: string;
|
||||
suspension?: SubscriptionSuspensionResponse;
|
||||
providerType: ProviderType;
|
||||
paymentSource?: PaymentSourceResponse;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -38,6 +41,10 @@ export class ProviderSubscriptionResponse extends BaseResponse {
|
||||
this.suspension = new SubscriptionSuspensionResponse(suspension);
|
||||
}
|
||||
this.providerType = this.getResponseProperty("providerType");
|
||||
const paymentSource = this.getResponseProperty("paymentSource");
|
||||
if (paymentSource != null) {
|
||||
this.paymentSource = new PaymentSourceResponse(paymentSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
|
||||
import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response";
|
||||
import { ErrorResponse } from "../../models/response/error.response";
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { BillingApiServiceAbstraction } from "../abstractions";
|
||||
import { PaymentMethodType } from "../enums";
|
||||
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
|
||||
@@ -23,11 +22,7 @@ import { PlanResponse } from "../models/response/plan.response";
|
||||
import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response";
|
||||
|
||||
export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private logService: LogService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
cancelOrganizationSubscription(
|
||||
organizationId: string,
|
||||
@@ -89,14 +84,12 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
}
|
||||
|
||||
async getOrganizationPaymentMethod(organizationId: string): Promise<PaymentMethodResponse> {
|
||||
const response = await this.execute(() =>
|
||||
this.apiService.send(
|
||||
"GET",
|
||||
"/organizations/" + organizationId + "/billing/payment-method",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/organizations/" + organizationId + "/billing/payment-method",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new PaymentMethodResponse(response);
|
||||
}
|
||||
@@ -120,38 +113,49 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
async getProviderClientOrganizations(
|
||||
providerId: string,
|
||||
): Promise<ListResponse<ProviderOrganizationOrganizationDetailsResponse>> {
|
||||
const response = await this.execute(() =>
|
||||
this.apiService.send("GET", "/providers/" + providerId + "/organizations", null, true, true),
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/organizations",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new ListResponse(response, ProviderOrganizationOrganizationDetailsResponse);
|
||||
}
|
||||
|
||||
async getProviderInvoices(providerId: string): Promise<InvoicesResponse> {
|
||||
const response = await this.execute(() =>
|
||||
this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/invoices",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/invoices",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new InvoicesResponse(response);
|
||||
}
|
||||
|
||||
async getProviderSubscription(providerId: string): Promise<ProviderSubscriptionResponse> {
|
||||
const response = await this.execute(() =>
|
||||
this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/subscription",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/subscription",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new ProviderSubscriptionResponse(response);
|
||||
}
|
||||
|
||||
async getProviderTaxInformation(providerId: string): Promise<TaxInfoResponse> {
|
||||
const response = await this.apiService.send(
|
||||
"GET",
|
||||
"/providers/" + providerId + "/billing/tax-information",
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return new TaxInfoResponse(response);
|
||||
}
|
||||
|
||||
async updateOrganizationPaymentMethod(
|
||||
organizationId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
@@ -192,6 +196,19 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async updateProviderPaymentMethod(
|
||||
providerId: string,
|
||||
request: UpdatePaymentMethodRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
"/providers/" + providerId + "/billing/payment-method",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async updateProviderTaxInformation(providerId: string, request: ExpandedTaxInfoUpdateRequest) {
|
||||
return await this.apiService.send(
|
||||
"PUT",
|
||||
@@ -215,6 +232,19 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async verifyProviderBankAccount(
|
||||
providerId: string,
|
||||
request: VerifyBankAccountRequest,
|
||||
): Promise<void> {
|
||||
return await this.apiService.send(
|
||||
"POST",
|
||||
"/providers/" + providerId + "/billing/payment-method/verify-bank-account",
|
||||
request,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async restartSubscription(
|
||||
organizationId: string,
|
||||
request: OrganizationCreateRequest,
|
||||
@@ -227,20 +257,4 @@ export class BillingApiService implements BillingApiServiceAbstraction {
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
private async execute(request: () => Promise<any>): Promise<any> {
|
||||
try {
|
||||
return await request();
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
if (error instanceof ErrorResponse) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: error.getSingleMessage(),
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
export enum FeatureFlag {
|
||||
/* Admin Console Team */
|
||||
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
|
||||
AccountDeprovisioning = "pm-10308-account-deprovisioning",
|
||||
VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint",
|
||||
LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission",
|
||||
@@ -18,7 +17,6 @@ export enum FeatureFlag {
|
||||
IdpAutoSubmitLogin = "idp-auto-submit-login",
|
||||
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
||||
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
||||
InlineMenuTotp = "inline-menu-totp",
|
||||
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
||||
NotificationRefresh = "notification-refresh",
|
||||
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
||||
@@ -27,27 +25,31 @@ export enum FeatureFlag {
|
||||
ItemShare = "item-share",
|
||||
CriticalApps = "pm-14466-risk-insights-critical-application",
|
||||
EnableRiskInsightsNotifications = "enable-risk-insights-notifications",
|
||||
DesktopSendUIRefresh = "desktop-send-ui-refresh",
|
||||
ExportAttachments = "export-attachments",
|
||||
|
||||
AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section",
|
||||
ExtensionRefresh = "extension-refresh",
|
||||
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
||||
VaultBulkManagementAction = "vault-bulk-management-action",
|
||||
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
||||
SSHKeyVaultItem = "ssh-key-vault-item",
|
||||
SSHAgent = "ssh-agent",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
TrialPaymentOptional = "PM-8163-trial-payment",
|
||||
SecurityTasks = "security-tasks",
|
||||
/* Vault */
|
||||
PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge",
|
||||
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
|
||||
NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss",
|
||||
NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss",
|
||||
VaultBulkManagementAction = "vault-bulk-management-action",
|
||||
SecurityTasks = "security-tasks",
|
||||
|
||||
/* Auth */
|
||||
PM9112_DeviceApprovalPersistence = "pm-9112-device-approval-persistence",
|
||||
|
||||
PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service",
|
||||
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
TrialPaymentOptional = "PM-8163-trial-payment",
|
||||
MacOsNativeCredentialSync = "macos-native-credential-sync",
|
||||
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
|
||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||
ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs",
|
||||
AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner",
|
||||
PM15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal",
|
||||
RecoveryCodeLogin = "pm-17128-recovery-code-login",
|
||||
PM12276_BreadcrumbEventLogs = "pm-12276-breadcrumbing-for-business-features",
|
||||
PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@@ -63,7 +65,6 @@ const FALSE = false as boolean;
|
||||
*/
|
||||
export const DefaultFeatureFlagValue = {
|
||||
/* Admin Console Team */
|
||||
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
|
||||
[FeatureFlag.AccountDeprovisioning]: FALSE,
|
||||
[FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE,
|
||||
[FeatureFlag.LimitItemDeletion]: FALSE,
|
||||
@@ -76,7 +77,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
|
||||
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
||||
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
||||
[FeatureFlag.InlineMenuTotp]: FALSE,
|
||||
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
|
||||
[FeatureFlag.NotificationRefresh]: FALSE,
|
||||
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
||||
@@ -85,27 +85,31 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.ItemShare]: FALSE,
|
||||
[FeatureFlag.CriticalApps]: FALSE,
|
||||
[FeatureFlag.EnableRiskInsightsNotifications]: FALSE,
|
||||
[FeatureFlag.DesktopSendUIRefresh]: FALSE,
|
||||
[FeatureFlag.ExportAttachments]: FALSE,
|
||||
|
||||
[FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE,
|
||||
[FeatureFlag.ExtensionRefresh]: FALSE,
|
||||
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
||||
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
||||
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
||||
[FeatureFlag.SSHKeyVaultItem]: FALSE,
|
||||
[FeatureFlag.SSHAgent]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.TrialPaymentOptional]: FALSE,
|
||||
[FeatureFlag.SecurityTasks]: FALSE,
|
||||
/* Vault */
|
||||
[FeatureFlag.PM8851_BrowserOnboardingNudge]: FALSE,
|
||||
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
|
||||
[FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE,
|
||||
[FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE,
|
||||
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
||||
[FeatureFlag.SecurityTasks]: FALSE,
|
||||
|
||||
/* Auth */
|
||||
[FeatureFlag.PM9112_DeviceApprovalPersistence]: FALSE,
|
||||
|
||||
[FeatureFlag.PM4154_BulkEncryptionService]: FALSE,
|
||||
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.TrialPaymentOptional]: FALSE,
|
||||
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,
|
||||
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
|
||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||
[FeatureFlag.ResellerManagedOrgAlert]: FALSE,
|
||||
[FeatureFlag.AccountDeprovisioningBanner]: FALSE,
|
||||
[FeatureFlag.PM15179_AddExistingOrgsFromProviderPortal]: FALSE,
|
||||
[FeatureFlag.RecoveryCodeLogin]: FALSE,
|
||||
[FeatureFlag.PM12276_BreadcrumbEventLogs]: FALSE,
|
||||
[FeatureFlag.PM18794_ProviderPaymentMethod]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
@@ -36,7 +36,6 @@ export abstract class EncryptService {
|
||||
): Promise<Uint8Array | null>;
|
||||
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
|
||||
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
|
||||
abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey;
|
||||
/**
|
||||
* @deprecated Replaced by BulkEncryptService, remove once the feature is tested and the featureflag PM-4154-multi-worker-encryption-service is removed
|
||||
* @param items The items to decrypt
|
||||
|
||||
@@ -78,8 +78,6 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
throw new Error("No key provided for decryption.");
|
||||
}
|
||||
|
||||
key = this.resolveLegacyKey(key, encString);
|
||||
|
||||
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
|
||||
if (key.macKey != null && encString?.mac == null) {
|
||||
this.logService.error(
|
||||
@@ -145,8 +143,6 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
throw new Error("Nothing provided for decryption.");
|
||||
}
|
||||
|
||||
key = this.resolveLegacyKey(key, encThing);
|
||||
|
||||
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
|
||||
if (key.macKey != null && encThing.macBytes == null) {
|
||||
this.logService.error(
|
||||
@@ -298,19 +294,4 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
this.logService.error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform into new key for the old encrypt-then-mac scheme if required, otherwise return the current key unchanged
|
||||
* @param encThing The encrypted object (e.g. encString or encArrayBuffer) that you want to decrypt
|
||||
*/
|
||||
resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey {
|
||||
if (
|
||||
encThing.encryptionType === EncryptionType.AesCbc128_HmacSha256_B64 &&
|
||||
key.encType === EncryptionType.AesCbc256_B64
|
||||
) {
|
||||
return new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +325,25 @@ describe("EncryptService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("decryptToUtf8", () => {
|
||||
it("throws if no key is provided", () => {
|
||||
return expect(encryptService.decryptToUtf8(null, null)).rejects.toThrow(
|
||||
"No key provided for decryption.",
|
||||
);
|
||||
});
|
||||
it("returns null if key is mac key but encstring has no mac", async () => {
|
||||
const key = new SymmetricCryptoKey(
|
||||
makeStaticByteArray(64, 0),
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
);
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
|
||||
const actual = await encryptService.decryptToUtf8(encString, key);
|
||||
expect(actual).toBeNull();
|
||||
expect(logService.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("rsa", () => {
|
||||
const data = makeStaticByteArray(10, 100);
|
||||
const encryptedData = makeStaticByteArray(10, 150);
|
||||
@@ -370,17 +389,16 @@ describe("EncryptService", () => {
|
||||
return expect(encryptService.rsaDecrypt(encString, null)).rejects.toThrow("No private key");
|
||||
});
|
||||
|
||||
it.each([
|
||||
EncryptionType.AesCbc256_B64,
|
||||
EncryptionType.AesCbc128_HmacSha256_B64,
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
])("throws if encryption type is %s", async (encType) => {
|
||||
encString.encryptionType = encType;
|
||||
it.each([EncryptionType.AesCbc256_B64, EncryptionType.AesCbc256_HmacSha256_B64])(
|
||||
"throws if encryption type is %s",
|
||||
async (encType) => {
|
||||
encString.encryptionType = encType;
|
||||
|
||||
await expect(encryptService.rsaDecrypt(encString, privateKey)).rejects.toThrow(
|
||||
"Invalid encryption type",
|
||||
);
|
||||
});
|
||||
await expect(encryptService.rsaDecrypt(encString, privateKey)).rejects.toThrow(
|
||||
"Invalid encryption type",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("decrypts data with provided key", async () => {
|
||||
cryptoFunctionService.rsaDecrypt.mockResolvedValue(data);
|
||||
@@ -398,30 +416,6 @@ describe("EncryptService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveLegacyKey", () => {
|
||||
it("creates a legacy key if required", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(32), EncryptionType.AesCbc256_B64);
|
||||
const encString = mock<EncString>();
|
||||
encString.encryptionType = EncryptionType.AesCbc128_HmacSha256_B64;
|
||||
|
||||
const actual = encryptService.resolveLegacyKey(key, encString);
|
||||
|
||||
const expected = new SymmetricCryptoKey(key.key, EncryptionType.AesCbc128_HmacSha256_B64);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("does not create a legacy key if not required", async () => {
|
||||
const encType = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64), encType);
|
||||
const encString = mock<EncString>();
|
||||
encString.encryptionType = encType;
|
||||
|
||||
const actual = encryptService.resolveLegacyKey(key, encString);
|
||||
|
||||
expect(actual).toEqual(key);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hash", () => {
|
||||
it("hashes a string and returns b64", async () => {
|
||||
cryptoFunctionService.hash.mockResolvedValue(Uint8Array.from([1, 2, 3]));
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { DeviceKey, UserKey } from "../../types/key";
|
||||
|
||||
import { DeviceResponse } from "./devices/responses/device.response";
|
||||
import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { DeviceKey, UserKey } from "../../../types/key";
|
||||
|
||||
export abstract class DeviceTrustServiceAbstraction {
|
||||
/**
|
||||
@@ -16,6 +15,12 @@ export abstract class DeviceTrustServiceAbstraction {
|
||||
*/
|
||||
supportsDeviceTrust$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Emits when a device has been trusted. This emission is specifically for the purpose of notifying
|
||||
* the consuming component to display a toast informing the user the device has been trusted.
|
||||
*/
|
||||
deviceTrusted$: Observable<void>;
|
||||
|
||||
/**
|
||||
* @description Checks if the device trust feature is supported for the given user.
|
||||
*/
|
||||
@@ -1,34 +1,34 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
import { firstValueFrom, map, Observable, Subject } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { AbstractStorageService } from "../../platform/abstractions/storage.service";
|
||||
import { StorageLocation } from "../../platform/enums";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { StorageOptions } from "../../platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { DEVICE_TRUST_DISK_LOCAL, StateProvider, UserKeyDefinition } from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { UserKey, DeviceKey } from "../../types/key";
|
||||
import { DeviceTrustServiceAbstraction } from "../abstractions/device-trust.service.abstraction";
|
||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
|
||||
import { SecretVerificationRequest } from "../models/request/secret-verification.request";
|
||||
import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response";
|
||||
import { DevicesApiServiceAbstraction } from "../../../auth/abstractions/devices-api.service.abstraction";
|
||||
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
|
||||
import {
|
||||
DeviceKeysUpdateRequest,
|
||||
UpdateDevicesTrustRequest,
|
||||
} from "../models/request/update-devices-trust.request";
|
||||
} from "../../../auth/models/request/update-devices-trust.request";
|
||||
import { AppIdService } from "../../../platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "../../../platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
|
||||
import { AbstractStorageService } from "../../../platform/abstractions/storage.service";
|
||||
import { StorageLocation } from "../../../platform/enums";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { StorageOptions } from "../../../platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { DEVICE_TRUST_DISK_LOCAL, StateProvider, UserKeyDefinition } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { UserKey, DeviceKey } from "../../../types/key";
|
||||
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
|
||||
import { DeviceTrustServiceAbstraction } from "../abstractions/device-trust.service.abstraction";
|
||||
|
||||
/** Uses disk storage so that the device key can persist after log out and tab removal. */
|
||||
export const DEVICE_KEY = new UserKeyDefinition<DeviceKey | null>(
|
||||
@@ -63,6 +63,10 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
|
||||
supportsDeviceTrust$: Observable<boolean>;
|
||||
|
||||
// Observable emission is used to trigger a toast in consuming components
|
||||
private deviceTrustedSubject = new Subject<void>();
|
||||
deviceTrusted$ = this.deviceTrustedSubject.asObservable();
|
||||
|
||||
constructor(
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private cryptoFunctionService: CryptoFunctionService,
|
||||
@@ -177,7 +181,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
// store device key in local/secure storage if enc keys posted to server successfully
|
||||
await this.setDeviceKey(userId, deviceKey);
|
||||
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("deviceTrusted"));
|
||||
// This emission will be picked up by consuming components to handle displaying a toast to the user
|
||||
this.deviceTrustedSubject.next();
|
||||
|
||||
return deviceResponse;
|
||||
}
|
||||
@@ -3,38 +3,38 @@
|
||||
import { matches, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import {
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
UserDecryptionOptions,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||
import { FakeActiveUserState } from "../../../spec/fake-state";
|
||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||
import { DeviceType } from "../../enums";
|
||||
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { AppIdService } from "../../platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService } from "../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
|
||||
import { AbstractStorageService } from "../../platform/abstractions/storage.service";
|
||||
import { StorageLocation } from "../../platform/enums";
|
||||
import { EncryptionType } from "../../platform/enums/encryption-type.enum";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { StorageOptions } from "../../platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../types/csprng";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { DeviceKey, UserKey } from "../../types/key";
|
||||
import { DeviceResponse } from "../abstractions/devices/responses/device.response";
|
||||
import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction";
|
||||
import { UpdateDevicesTrustRequest } from "../models/request/update-devices-trust.request";
|
||||
import { ProtectedDeviceResponse } from "../models/response/protected-device.response";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service";
|
||||
import { FakeActiveUserState } from "../../../../spec/fake-state";
|
||||
import { FakeStateProvider } from "../../../../spec/fake-state-provider";
|
||||
import { DeviceResponse } from "../../../auth/abstractions/devices/responses/device.response";
|
||||
import { DevicesApiServiceAbstraction } from "../../../auth/abstractions/devices-api.service.abstraction";
|
||||
import { UpdateDevicesTrustRequest } from "../../../auth/models/request/update-devices-trust.request";
|
||||
import { ProtectedDeviceResponse } from "../../../auth/models/response/protected-device.response";
|
||||
import { DeviceType } from "../../../enums";
|
||||
import { AppIdService } from "../../../platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "../../../platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService } from "../../../platform/abstractions/crypto-function.service";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
|
||||
import { AbstractStorageService } from "../../../platform/abstractions/storage.service";
|
||||
import { StorageLocation } from "../../../platform/enums";
|
||||
import { EncryptionType } from "../../../platform/enums/encryption-type.enum";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { StorageOptions } from "../../../platform/models/domain/storage-options";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "../../../types/csprng";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { DeviceKey, UserKey } from "../../../types/key";
|
||||
import { EncryptService } from "../../crypto/abstractions/encrypt.service";
|
||||
|
||||
import {
|
||||
SHOULD_TRUST_DEVICE,
|
||||
@@ -1,8 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Organization } from "../../admin-console/models/domain/organization";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
import { Organization } from "../../../admin-console/models/domain/organization";
|
||||
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
|
||||
import { UserId } from "../../../types/guid";
|
||||
|
||||
export abstract class KeyConnectorService {
|
||||
setMasterKeyFromUrl: (url: string, userId: UserId) => Promise<void>;
|
||||
@@ -4,27 +4,27 @@ import { of } from "rxjs";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { OrganizationData } from "../../admin-console/models/data/organization.data";
|
||||
import { Organization } from "../../admin-console/models/domain/organization";
|
||||
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
import { KeyGenerationService } from "../../platform/services/key-generation.service";
|
||||
import { OrganizationId, UserId } from "../../types/guid";
|
||||
import { MasterKey } from "../../types/key";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request";
|
||||
import { KeyConnectorUserKeyResponse } from "../models/response/key-connector-user-key.response";
|
||||
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
||||
import { ApiService } from "../../../abstractions/api.service";
|
||||
import { OrganizationData } from "../../../admin-console/models/data/organization.data";
|
||||
import { Organization } from "../../../admin-console/models/domain/organization";
|
||||
import { ProfileOrganizationResponse } from "../../../admin-console/models/response/profile-organization.response";
|
||||
import { KeyConnectorUserKeyResponse } from "../../../auth/models/response/key-connector-user-key.response";
|
||||
import { TokenService } from "../../../auth/services/token.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import { KeyGenerationService } from "../../../platform/services/key-generation.service";
|
||||
import { OrganizationId, UserId } from "../../../types/guid";
|
||||
import { MasterKey } from "../../../types/key";
|
||||
import { FakeMasterPasswordService } from "../../master-password/services/fake-master-password.service";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request";
|
||||
|
||||
import {
|
||||
USES_KEY_CONNECTOR,
|
||||
CONVERT_ACCOUNT_TO_KEY_CONNECTOR,
|
||||
KeyConnectorService,
|
||||
} from "./key-connector.service";
|
||||
import { FakeMasterPasswordService } from "./master-password/fake-master-password.service";
|
||||
import { TokenService } from "./token.service";
|
||||
|
||||
describe("KeyConnectorService", () => {
|
||||
let keyConnectorService: KeyConnectorService;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user