1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

fix(two-factor) [PM-21204]: Users without premium cannot disable premium 2FA (#17134)

* refactor(two-factor-service) [PM-21204]: Stub API methods in TwoFactorService (domain).

* refactor(two-factor-service) [PM-21204]: Build out stubs and add documentation.

* refactor(two-factor-service) [PM-21204]: Update TwoFactorApiService call sites to use TwoFactorService.

* refactor(two-fatcor) [PM-21204]: Remove deprecated and unused formPromise methods.

* refactor(two-factor) [PM-21204]: Move 2FA-supporting services into common/auth/two-factor feature namespace.

* refactor(two-factor) [PM-21204]: Update imports for service/init containers.

* feat(two-factor) [PM-21204]: Add a disabling flow for Premium 2FA when enabled on a non-Premium account.

* fix(two-factor-service) [PM-21204]: Fix type-safety of module constants.

* fix(multiple) [PM-21204]: Prettier.

* fix(user-verification-dialog) [PM-21204]: Remove bodyText configuration for this use.

* fix(user-verification-dialog) [PM-21204]: Improve the error message displayed to the user.
This commit is contained in:
Dave
2025-11-21 10:35:34 -05:00
committed by GitHub
parent 490ef1dab0
commit daf7b7d2ce
47 changed files with 966 additions and 441 deletions

View File

@@ -2,7 +2,7 @@ import { DOCUMENT } from "@angular/common";
import { inject, Inject, Injectable } from "@angular/core";
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -20,7 +20,6 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
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";
@@ -28,7 +27,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService, TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { ClientType } from "@bitwarden/common/enums";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";

View File

@@ -49,10 +49,14 @@ import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/defau
import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation";
import { MasterPasswordApiService } from "@bitwarden/common/auth/services/master-password/master-password-api.service.implementation";
import { TokenService } from "@bitwarden/common/auth/services/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service";
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
import { TwoFactorApiService, DefaultTwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import {
DefaultTwoFactorService,
TwoFactorService,
TwoFactorApiService,
DefaultTwoFactorApiService,
} from "@bitwarden/common/auth/two-factor";
import {
AutofillSettingsService,
AutofillSettingsServiceAbstraction,
@@ -627,10 +631,11 @@ export class ServiceContainer {
this.stateProvider,
);
this.twoFactorService = new TwoFactorService(
this.twoFactorService = new DefaultTwoFactorService(
this.i18nService,
this.platformUtilsService,
this.globalStateProvider,
this.twoFactorApiService,
);
const sdkClientFactory = flagEnabled("sdk")

View File

@@ -6,7 +6,7 @@ import { AbstractThemingService } from "@bitwarden/angular/platform/services/the
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -39,7 +39,7 @@ export class InitService {
private vaultTimeoutService: DefaultVaultTimeoutService,
private i18nService: I18nServiceAbstraction,
private eventUploadService: EventUploadServiceAbstraction,
private twoFactorService: TwoFactorServiceAbstraction,
private twoFactorService: TwoFactorService,
private notificationsService: ServerNotificationsService,
private platformUtilsService: PlatformUtilsServiceAbstraction,
private stateService: StateServiceAbstraction,

View File

@@ -11,16 +11,17 @@ import {
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogRef, DialogService } from "@bitwarden/components";
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
import { TwoFactorSetupDuoComponent } from "../../../auth/settings/two-factor/two-factor-setup-duo.component";
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor/two-factor-setup.component";
@@ -37,7 +38,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
tabbedHeader = false;
constructor(
dialogService: DialogService,
twoFactorApiService: TwoFactorApiService,
twoFactorService: TwoFactorService,
messagingService: MessagingService,
policyService: PolicyService,
private route: ActivatedRoute,
@@ -46,16 +47,20 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
protected accountService: AccountService,
configService: ConfigService,
i18nService: I18nService,
protected userVerificationService: UserVerificationService,
protected toastService: ToastService,
) {
super(
dialogService,
twoFactorApiService,
twoFactorService,
messagingService,
policyService,
billingAccountProfileStateService,
accountService,
configService,
i18nService,
userVerificationService,
toastService,
);
}
@@ -118,7 +123,7 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent impleme
}
protected getTwoFactorProviders() {
return this.twoFactorApiService.getTwoFactorOrganizationProviders(this.organizationId);
return this.twoFactorService.getTwoFactorOrganizationProviders(this.organizationId);
}
protected filterProvider(type: TwoFactorProviderType): boolean {

View File

@@ -7,7 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorProviderResponse } from "@bitwarden/common/auth/models/response/two-factor-provider.response";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -23,14 +23,14 @@ describe("ChangeEmailComponent", () => {
let fixture: ComponentFixture<ChangeEmailComponent>;
let apiService: MockProxy<ApiService>;
let twoFactorApiService: MockProxy<TwoFactorApiService>;
let twoFactorService: MockProxy<TwoFactorService>;
let accountService: FakeAccountService;
let keyService: MockProxy<KeyService>;
let kdfConfigService: MockProxy<KdfConfigService>;
beforeEach(async () => {
apiService = mock<ApiService>();
twoFactorApiService = mock<TwoFactorApiService>();
twoFactorService = mock<TwoFactorService>();
keyService = mock<KeyService>();
kdfConfigService = mock<KdfConfigService>();
accountService = mockAccountServiceWith("UserId" as UserId);
@@ -40,7 +40,7 @@ describe("ChangeEmailComponent", () => {
providers: [
{ provide: AccountService, useValue: accountService },
{ provide: ApiService, useValue: apiService },
{ provide: TwoFactorApiService, useValue: twoFactorApiService },
{ provide: TwoFactorService, useValue: twoFactorService },
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: KeyService, useValue: keyService },
{ provide: MessagingService, useValue: mock<MessagingService>() },
@@ -61,7 +61,7 @@ describe("ChangeEmailComponent", () => {
describe("ngOnInit", () => {
beforeEach(() => {
twoFactorApiService.getTwoFactorProviders.mockResolvedValue({
twoFactorService.getEnabledTwoFactorProviders.mockResolvedValue({
data: [{ type: TwoFactorProviderType.Email, enabled: true } as TwoFactorProviderResponse],
} as ListResponse<TwoFactorProviderResponse>);
});

View File

@@ -8,7 +8,7 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p
import { EmailTokenRequest } from "@bitwarden/common/auth/models/request/email-token.request";
import { EmailRequest } from "@bitwarden/common/auth/models/request/email.request";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { UserId } from "@bitwarden/common/types/guid";
@@ -40,7 +40,7 @@ export class ChangeEmailComponent implements OnInit {
constructor(
private accountService: AccountService,
private apiService: ApiService,
private twoFactorApiService: TwoFactorApiService,
private twoFactorService: TwoFactorService,
private i18nService: I18nService,
private keyService: KeyService,
private messagingService: MessagingService,
@@ -52,7 +52,7 @@ export class ChangeEmailComponent implements OnInit {
async ngOnInit() {
this.userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const twoFactorProviders = await this.twoFactorApiService.getTwoFactorProviders();
const twoFactorProviders = await this.twoFactorService.getEnabledTwoFactorProviders();
this.showTwoFactorEmailWarning = twoFactorProviders.data.some(
(p) => p.type === TwoFactorProviderType.Email && p.enabled,
);

View File

@@ -9,7 +9,7 @@ import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-a
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { SetVerifyDevicesRequest } from "@bitwarden/common/auth/models/request/set-verify-devices.request";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { Verification } from "@bitwarden/common/auth/types/verification";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -66,7 +66,7 @@ export class SetAccountVerifyDevicesDialogComponent implements OnInit, OnDestroy
private userVerificationService: UserVerificationService,
private dialogRef: DialogRef,
private toastService: ToastService,
private twoFactorApiService: TwoFactorApiService,
private twoFactorService: TwoFactorService,
) {
this.accountService.accountVerifyNewDeviceLogin$
.pipe(takeUntil(this.destroy$))
@@ -76,7 +76,7 @@ export class SetAccountVerifyDevicesDialogComponent implements OnInit, OnDestroy
}
async ngOnInit() {
const twoFactorProviders = await this.twoFactorApiService.getTwoFactorProviders();
const twoFactorProviders = await this.twoFactorService.getEnabledTwoFactorProviders();
this.has2faConfigured = twoFactorProviders.data.length > 0;
}

View File

@@ -12,7 +12,7 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p
import { DisableTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/disable-two-factor-authenticator.request";
import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/update-two-factor-authenticator.request";
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -96,7 +96,7 @@ export class TwoFactorSetupAuthenticatorComponent
constructor(
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorAuthenticatorResponse>,
private dialogRef: DialogRef,
twoFactorApiService: TwoFactorApiService,
twoFactorService: TwoFactorService,
i18nService: I18nService,
userVerificationService: UserVerificationService,
private formBuilder: FormBuilder,
@@ -108,7 +108,7 @@ export class TwoFactorSetupAuthenticatorComponent
protected toastService: ToastService,
) {
super(
twoFactorApiService,
twoFactorService,
i18nService,
platformUtilsService,
logService,
@@ -158,7 +158,7 @@ export class TwoFactorSetupAuthenticatorComponent
request.key = this.key;
request.userVerificationToken = this.userVerificationToken;
const response = await this.twoFactorApiService.putTwoFactorAuthenticator(request);
const response = await this.twoFactorService.putTwoFactorAuthenticator(request);
await this.processResponse(response);
this.onUpdated.emit(true);
}
@@ -178,7 +178,7 @@ export class TwoFactorSetupAuthenticatorComponent
request.type = this.type;
request.key = this.key;
request.userVerificationToken = this.userVerificationToken;
await this.twoFactorApiService.deleteTwoFactorAuthenticator(request);
await this.twoFactorService.deleteTwoFactorAuthenticator(request);
this.enabled = false;
this.toastService.showToast({
variant: "success",

View File

@@ -6,7 +6,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { UpdateTwoFactorDuoRequest } from "@bitwarden/common/auth/models/request/update-two-factor-duo.request";
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -67,7 +67,7 @@ export class TwoFactorSetupDuoComponent
constructor(
@Inject(DIALOG_DATA) protected data: TwoFactorDuoComponentConfig,
twoFactorApiService: TwoFactorApiService,
twoFactorService: TwoFactorService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
@@ -78,7 +78,7 @@ export class TwoFactorSetupDuoComponent
protected toastService: ToastService,
) {
super(
twoFactorApiService,
twoFactorService,
i18nService,
platformUtilsService,
logService,
@@ -143,12 +143,12 @@ export class TwoFactorSetupDuoComponent
let response: TwoFactorDuoResponse;
if (this.organizationId != null) {
response = await this.twoFactorApiService.putTwoFactorOrganizationDuo(
response = await this.twoFactorService.putTwoFactorOrganizationDuo(
this.organizationId,
request,
);
} else {
response = await this.twoFactorApiService.putTwoFactorDuo(request);
response = await this.twoFactorService.putTwoFactorDuo(request);
}
this.processResponse(response);

View File

@@ -9,7 +9,7 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { UpdateTwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/update-two-factor-email.request";
import { TwoFactorEmailResponse } from "@bitwarden/common/auth/models/response/two-factor-email.response";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -70,7 +70,7 @@ export class TwoFactorSetupEmailComponent
constructor(
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorEmailResponse>,
twoFactorApiService: TwoFactorApiService,
twoFactorService: TwoFactorService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
@@ -82,7 +82,7 @@ export class TwoFactorSetupEmailComponent
protected toastService: ToastService,
) {
super(
twoFactorApiService,
twoFactorService,
i18nService,
platformUtilsService,
logService,
@@ -135,7 +135,7 @@ export class TwoFactorSetupEmailComponent
sendEmail = async () => {
const request = await this.buildRequestModel(TwoFactorEmailRequest);
request.email = this.email;
this.emailPromise = this.twoFactorApiService.postTwoFactorEmailSetup(request);
this.emailPromise = this.twoFactorService.postTwoFactorEmailSetup(request);
await this.emailPromise;
this.sentEmail = this.email;
};
@@ -145,7 +145,7 @@ export class TwoFactorSetupEmailComponent
request.email = this.email;
request.token = this.token;
const response = await this.twoFactorApiService.putTwoFactorEmail(request);
const response = await this.twoFactorService.putTwoFactorEmail(request);
await this.processResponse(response);
this.onUpdated.emit(true);
}

View File

@@ -5,7 +5,7 @@ import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-p
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
import { TwoFactorProviderRequest } from "@bitwarden/common/auth/models/request/two-factor-provider.request";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { AuthResponseBase } from "@bitwarden/common/auth/types/auth-response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -32,7 +32,7 @@ export abstract class TwoFactorSetupMethodBaseComponent {
protected componentName = "";
constructor(
protected twoFactorApiService: TwoFactorApiService,
protected twoFactorService: TwoFactorService,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected logService: LogService,
@@ -47,58 +47,6 @@ export abstract class TwoFactorSetupMethodBaseComponent {
this.authed = true;
}
/** @deprecated used for formPromise flows.*/
protected async enable(enableFunction: () => Promise<void>) {
try {
await enableFunction();
this.onUpdated.emit(true);
} catch (e) {
this.logService.error(e);
}
}
/**
* @deprecated used for formPromise flows.
* TODO: Remove this method when formPromises are removed from all flows.
* */
protected async disable(promise: Promise<unknown>) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "disable" },
content: { key: "twoStepDisableDesc" },
type: "warning",
});
if (!confirmed) {
return;
}
try {
const request = await this.buildRequestModel(TwoFactorProviderRequest);
if (this.type === undefined) {
throw new Error("Two-factor provider type is required");
}
request.type = this.type;
if (this.organizationId != null) {
promise = this.twoFactorApiService.putTwoFactorOrganizationDisable(
this.organizationId,
request,
);
} else {
promise = this.twoFactorApiService.putTwoFactorDisable(request);
}
await promise;
this.enabled = false;
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("twoStepDisabled"),
});
this.onUpdated.emit(false);
} catch (e) {
this.logService.error(e);
}
}
protected async disableMethod() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "disable" },
@@ -116,9 +64,9 @@ export abstract class TwoFactorSetupMethodBaseComponent {
}
request.type = this.type;
if (this.organizationId != null) {
await this.twoFactorApiService.putTwoFactorOrganizationDisable(this.organizationId, request);
await this.twoFactorService.putTwoFactorOrganizationDisable(this.organizationId, request);
} else {
await this.twoFactorApiService.putTwoFactorDisable(request);
await this.twoFactorService.putTwoFactorDisable(request);
}
this.enabled = false;
this.toastService.showToast({

View File

@@ -12,7 +12,7 @@ import {
ChallengeResponse,
TwoFactorWebAuthnResponse,
} from "@bitwarden/common/auth/models/response/two-factor-web-authn.response";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -72,7 +72,6 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
webAuthnListening: boolean = false;
webAuthnResponse: PublicKeyCredential | null = null;
challengePromise: Promise<ChallengeResponse> | undefined;
formPromise: Promise<TwoFactorWebAuthnResponse> | undefined;
override componentName = "app-two-factor-webauthn";
@@ -81,7 +80,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
constructor(
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorWebAuthnResponse>,
private dialogRef: DialogRef,
twoFactorApiService: TwoFactorApiService,
twoFactorService: TwoFactorService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
private ngZone: NgZone,
@@ -91,7 +90,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
toastService: ToastService,
) {
super(
twoFactorApiService,
twoFactorService,
i18nService,
platformUtilsService,
logService,
@@ -129,7 +128,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
request.id = this.keyIdAvailable;
request.name = this.formGroup.value.name || "";
const response = await this.twoFactorApiService.putTwoFactorWebAuthn(request);
const response = await this.twoFactorService.putTwoFactorWebAuthn(request);
this.processResponse(response);
this.toastService.showToast({
title: this.i18nService.t("success"),
@@ -165,7 +164,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnDeleteRequest);
request.id = key.id;
try {
key.removePromise = this.twoFactorApiService.deleteTwoFactorWebAuthn(request);
key.removePromise = this.twoFactorService.deleteTwoFactorWebAuthn(request);
const response = await key.removePromise;
key.removePromise = null;
await this.processResponse(response);
@@ -179,7 +178,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
return;
}
const request = await this.buildRequestModel(SecretVerificationRequest);
this.challengePromise = this.twoFactorApiService.getTwoFactorWebAuthnChallenge(request);
this.challengePromise = this.twoFactorService.getTwoFactorWebAuthnChallenge(request);
const challenge = await this.challengePromise;
this.readDevice(challenge);
};

View File

@@ -13,7 +13,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { UpdateTwoFactorYubikeyOtpRequest } from "@bitwarden/common/auth/models/request/update-two-factor-yubikey-otp.request";
import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -74,9 +74,6 @@ export class TwoFactorSetupYubiKeyComponent
keys: Key[] = [];
anyKeyHasNfc = false;
formPromise: Promise<TwoFactorYubiKeyResponse> | undefined;
disablePromise: Promise<unknown> | undefined;
override componentName = "app-two-factor-yubikey";
formGroup:
| FormGroup<{
@@ -95,7 +92,7 @@ export class TwoFactorSetupYubiKeyComponent
constructor(
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorYubiKeyResponse>,
twoFactorApiService: TwoFactorApiService,
twoFactorService: TwoFactorService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
logService: LogService,
@@ -105,7 +102,7 @@ export class TwoFactorSetupYubiKeyComponent
protected toastService: ToastService,
) {
super(
twoFactorApiService,
twoFactorService,
i18nService,
platformUtilsService,
logService,
@@ -178,7 +175,7 @@ export class TwoFactorSetupYubiKeyComponent
request.key5 = keys != null && keys.length > 4 ? (keys[4]?.key ?? "") : "";
request.nfc = this.formGroup.value.anyKeyHasNfc ?? false;
this.processResponse(await this.twoFactorApiService.putTwoFactorYubiKey(request));
this.processResponse(await this.twoFactorService.putTwoFactorYubiKey(request));
this.refreshFormArrayData();
this.toastService.showToast({
title: this.i18nService.t("success"),

View File

@@ -71,15 +71,26 @@
<div class="tw-mt-2 tw-text-wrap">{{ p.description }}</div>
</div>
<bit-item-action slot="end">
<button
type="button"
bitButton
buttonType="secondary"
[disabled]="!(canAccessPremium$ | async) && p.premium"
(click)="manage(p.type)"
>
{{ "manage" | i18n }}
</button>
@if (p.premium && p.enabled && !(canAccessPremium$ | async)) {
<button
type="button"
bitButton
buttonType="danger"
(click)="disablePremium2faTypeForNonPremiumUser(p.type)"
>
{{ "disable" | i18n }}
</button>
} @else {
<button
type="button"
bitButton
buttonType="secondary"
[disabled]="!(canAccessPremium$ | async) && p.premium"
(click)="manage(p.type)"
>
{{ "manage" | i18n }}
</button>
}
</bit-item-action>
</bit-item>
</bit-item-group>

View File

@@ -12,26 +12,28 @@ import {
} from "rxjs";
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
import { UserVerificationDialogComponent } from "@bitwarden/auth/angular";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorProviderRequest } from "@bitwarden/common/auth/models/request/two-factor-provider.request";
import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response";
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
import { TwoFactorEmailResponse } from "@bitwarden/common/auth/models/response/two-factor-email.response";
import { TwoFactorWebAuthnResponse } from "@bitwarden/common/auth/models/response/two-factor-web-authn.response";
import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response/two-factor-yubi-key.response";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService, TwoFactorProviders } from "@bitwarden/common/auth/two-factor";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogRef, DialogService, ItemModule } from "@bitwarden/components";
import { DialogRef, DialogService, ItemModule, ToastService } from "@bitwarden/components";
import { HeaderModule } from "../../../layouts/header/header.module";
import { SharedModule } from "../../../shared/shared.module";
@@ -59,7 +61,6 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
recoveryCodeWarningMessage: string;
showPolicyWarning = false;
loading = true;
formPromise: Promise<any>;
tabbedHeader = true;
@@ -69,13 +70,15 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
constructor(
protected dialogService: DialogService,
protected twoFactorApiService: TwoFactorApiService,
protected twoFactorService: TwoFactorService,
protected messagingService: MessagingService,
protected policyService: PolicyService,
billingAccountProfileStateService: BillingAccountProfileStateService,
protected accountService: AccountService,
protected configService: ConfigService,
protected i18nService: I18nService,
protected userVerificationService: UserVerificationService,
protected toastService: ToastService,
) {
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
switchMap((account) =>
@@ -150,6 +153,50 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
return await lastValueFrom(twoFactorVerifyDialogRef.closed);
}
/**
* For users who enabled a premium-only 2fa provider,
* they should still be allowed to disable that provider
* (without otherwise modifying) if they no longer have
* premium access [PM-21204]
* @param type the 2FA Provider Type
*/
async disablePremium2faTypeForNonPremiumUser(type: TwoFactorProviderType) {
// Use UserVerificationDialogComponent instead of TwoFactorVerifyComponent
// because the latter makes GET API calls that require premium for YubiKey/Duo.
// The disable endpoint only requires user verification, not provider configuration.
const result = await UserVerificationDialogComponent.open(this.dialogService, {
title: "twoStepLogin",
verificationType: {
type: "custom",
verificationFn: async (secret) => {
const request = await this.userVerificationService.buildRequest<TwoFactorProviderRequest>(
secret,
TwoFactorProviderRequest,
);
request.type = type;
await this.twoFactorService.putTwoFactorDisable(request);
return true;
},
},
});
if (result.userAction === "cancel") {
return;
}
if (!result.verificationSuccess) {
return;
}
this.toastService.showToast({
variant: "success",
title: "",
message: this.i18nService.t("twoStepDisabled"),
});
this.updateStatus(false, type);
}
async manage(type: TwoFactorProviderType) {
// clear any existing subscriptions before creating a new one
this.twoFactorSetupSubscription?.unsubscribe();
@@ -264,7 +311,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
}
protected getTwoFactorProviders() {
return this.twoFactorApiService.getTwoFactorProviders();
return this.twoFactorService.getEnabledTwoFactorProviders();
}
protected filterProvider(type: TwoFactorProviderType): boolean {

View File

@@ -5,7 +5,7 @@ import { UserVerificationFormInputComponent } from "@bitwarden/auth/angular";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { TwoFactorResponse } from "@bitwarden/common/auth/types/two-factor-response";
import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification";
@@ -54,7 +54,7 @@ export class TwoFactorVerifyComponent {
constructor(
@Inject(DIALOG_DATA) protected data: TwoFactorVerifyDialogData,
private dialogRef: DialogRef,
private twoFactorApiService: TwoFactorApiService,
private twoFactorService: TwoFactorService,
private i18nService: I18nService,
private userVerificationService: UserVerificationService,
) {
@@ -110,22 +110,22 @@ export class TwoFactorVerifyComponent {
private apiCall(request: SecretVerificationRequest): Promise<TwoFactorResponse> {
switch (this.type) {
case -1 as TwoFactorProviderType:
return this.twoFactorApiService.getTwoFactorRecover(request);
return this.twoFactorService.getTwoFactorRecover(request);
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.OrganizationDuo:
if (this.organizationId != null) {
return this.twoFactorApiService.getTwoFactorOrganizationDuo(this.organizationId, request);
return this.twoFactorService.getTwoFactorOrganizationDuo(this.organizationId, request);
} else {
return this.twoFactorApiService.getTwoFactorDuo(request);
return this.twoFactorService.getTwoFactorDuo(request);
}
case TwoFactorProviderType.Email:
return this.twoFactorApiService.getTwoFactorEmail(request);
return this.twoFactorService.getTwoFactorEmail(request);
case TwoFactorProviderType.WebAuthn:
return this.twoFactorApiService.getTwoFactorWebAuthn(request);
return this.twoFactorService.getTwoFactorWebAuthn(request);
case TwoFactorProviderType.Authenticator:
return this.twoFactorApiService.getTwoFactorAuthenticator(request);
return this.twoFactorService.getTwoFactorAuthenticator(request);
case TwoFactorProviderType.Yubikey:
return this.twoFactorApiService.getTwoFactorYubiKey(request);
return this.twoFactorService.getTwoFactorYubiKey(request);
default:
throw new Error(`Unknown two-factor type: ${this.type}`);
}

View File

@@ -6,7 +6,7 @@ import { AbstractThemingService } from "@bitwarden/angular/platform/services/the
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -31,7 +31,7 @@ export class InitService {
private vaultTimeoutService: DefaultVaultTimeoutService,
private i18nService: I18nServiceAbstraction,
private eventUploadService: EventUploadServiceAbstraction,
private twoFactorService: TwoFactorServiceAbstraction,
private twoFactorService: TwoFactorService,
private keyService: KeyServiceAbstraction,
private themingService: AbstractThemingService,
private encryptService: EncryptService,

View File

@@ -12178,5 +12178,8 @@
},
"confirmNoSelectedCriticalApplicationsDesc": {
"message": "Are you sure you want to continue?"
},
"userVerificationFailed": {
"message": "User verification failed."
}
}