1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-21 11:53:34 +00:00

Merge branch 'main' into PM-26250-Explore-options-to-enable-direct-importer-for-mac-app-store-build

This commit is contained in:
John Harrington
2025-11-21 14:02:45 -07:00
committed by GitHub
76 changed files with 1800 additions and 1114 deletions

View File

@@ -363,6 +363,8 @@
"@types/jsdom",
"@types/papaparse",
"@types/zxcvbn",
"async-trait",
"clap",
"jsdom",
"jszip",
"oidc-client-ts",
@@ -435,5 +437,11 @@
description: "Higher versions of lowdb do not need separate types",
},
],
ignoreDeps: ["@types/koa-bodyparser", "bootstrap", "node-ipc", "@bitwarden/sdk-internal"],
ignoreDeps: [
"@types/koa-bodyparser",
"bootstrap",
"node-ipc",
"@bitwarden/sdk-internal",
"@bitwarden/commercial-sdk-internal",
],
}

View File

@@ -2949,17 +2949,21 @@ export class OverlayBackground implements OverlayBackgroundInterface {
(await this.checkFocusedFieldHasValue(port.sender.tab)) &&
(await this.shouldShowSaveLoginInlineMenuList(port.sender.tab));
const iframeUrl = chrome.runtime.getURL(
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`,
);
const styleSheetUrl = chrome.runtime.getURL(
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`,
);
const extensionOrigin = new URL(iframeUrl).origin;
this.postMessageToPort(port, {
command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`,
iframeUrl: chrome.runtime.getURL(
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`,
),
iframeUrl,
pageTitle: chrome.i18n.getMessage(
isInlineMenuListPort ? "bitwardenVault" : "bitwardenOverlayButton",
),
styleSheetUrl: chrome.runtime.getURL(
`overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`,
),
styleSheetUrl,
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
translations: this.getInlineMenuTranslations(),
ciphers: isInlineMenuListPort ? await this.getInlineMenuCipherData() : null,
@@ -2973,6 +2977,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
showSaveLoginMenu,
showInlineMenuAccountCreation,
authStatus,
extensionOrigin,
});
this.updateInlineMenuPosition(
port.sender,

View File

@@ -17,6 +17,7 @@ export type InitAutofillInlineMenuElementMessage = AutofillInlineMenuContainerMe
translations: Record<string, string>;
ciphers: InlineMenuCipherData[] | null;
portName: string;
extensionOrigin?: string;
};
export type AutofillInlineMenuContainerWindowMessage = AutofillInlineMenuContainerMessage &

View File

@@ -184,4 +184,38 @@ describe("AutofillInlineMenuContainer", () => {
expect(port.postMessage).not.toHaveBeenCalled();
});
});
describe("isExtensionUrlWithOrigin", () => {
it("validates extension URLs with matching origin", () => {
const url = "chrome-extension://test-id/path/to/file.html";
const origin = "chrome-extension://test-id";
expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"](url, origin)).toBe(true);
});
it("rejects extension URLs with mismatched origin", () => {
const url = "chrome-extension://test-id/path/to/file.html";
const origin = "chrome-extension://different-id";
expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"](url, origin)).toBe(false);
});
it("validates extension URL against its own origin when no expectedOrigin provided", () => {
const url = "moz-extension://test-id/path/to/file.html";
expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"](url)).toBe(true);
});
it("rejects non-extension protocols", () => {
const url = "https://example.com/path";
const origin = "https://example.com";
expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"](url, origin)).toBe(false);
});
it("rejects empty or invalid URLs", () => {
expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"]("")).toBe(false);
expect(autofillInlineMenuContainer["isExtensionUrlWithOrigin"]("not-a-url")).toBe(false);
});
});
});

View File

@@ -87,11 +87,13 @@ export class AutofillInlineMenuContainer {
return;
}
if (!this.isExtensionUrl(message.iframeUrl)) {
const expectedOrigin = message.extensionOrigin || this.extensionOrigin;
if (!this.isExtensionUrlWithOrigin(message.iframeUrl, expectedOrigin)) {
return;
}
if (message.styleSheetUrl && !this.isExtensionUrl(message.styleSheetUrl)) {
if (message.styleSheetUrl && !this.isExtensionUrlWithOrigin(message.styleSheetUrl)) {
return;
}
@@ -115,20 +117,25 @@ export class AutofillInlineMenuContainer {
}
/**
* validates that a URL is from the extension origin.
* prevents loading arbitrary URLs in the iframe.
* Validates that a URL uses an extension protocol and matches the expected extension origin.
* If no expectedOrigin is provided, validates against the URL's own origin.
*
* @param url - The URL to validate.
*/
private isExtensionUrl(url: string): boolean {
private isExtensionUrlWithOrigin(url: string, expectedOrigin?: string): boolean {
if (!url) {
return false;
}
try {
const urlObj = new URL(url);
return (
urlObj.origin === this.extensionOrigin || urlObj.href.startsWith(this.extensionOrigin + "/")
);
const isExtensionProtocol = /^[a-z]+(-[a-z]+)?-extension:$/i.test(urlObj.protocol);
if (!isExtensionProtocol) {
return false;
}
const originToValidate = expectedOrigin ?? urlObj.origin;
return urlObj.origin === originToValidate || urlObj.href.startsWith(originToValidate + "/");
} catch {
return false;
}

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

@@ -75,7 +75,7 @@
"inquirer": "8.2.6",
"jsdom": "26.1.0",
"jszip": "3.10.1",
"koa": "2.16.2",
"koa": "2.16.3",
"koa-bodyparser": "4.4.1",
"koa-json": "2.0.2",
"lowdb": "1.0.0",

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

@@ -5,6 +5,7 @@ import { LogService } from "@bitwarden/logging";
import { WindowMain } from "../../main/window.main";
import { stringIsNotUndefinedNullAndEmpty } from "../../utils";
import { AutotypeMatchError } from "../models/autotype-errors";
import { AutotypeVaultData } from "../models/autotype-vault-data";
import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut";
@@ -56,6 +57,14 @@ export class MainDesktopAutotypeService {
this.doAutotype(vaultData, this.autotypeKeyboardShortcut.getArrayFormat());
}
});
ipcMain.on("autofill.completeAutotypeError", (_event, matchError: AutotypeMatchError) => {
this.logService.debug(
"autofill.completeAutotypeError",
"No match for window: " + matchError.windowTitle,
);
this.logService.error("autofill.completeAutotypeError", matchError.errorMessage);
});
}
disableAutotype() {

View File

@@ -0,0 +1,8 @@
/**
* This error is surfaced when there is no matching
* vault item found.
*/
export interface AutotypeMatchError {
windowTitle: string;
errorMessage: string;
}

View File

@@ -5,6 +5,7 @@ import type { autofill } from "@bitwarden/desktop-napi";
import { Command } from "../platform/main/autofill/command";
import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main";
import { AutotypeMatchError } from "./models/autotype-errors";
import { AutotypeVaultData } from "./models/autotype-vault-data";
export default {
@@ -141,7 +142,7 @@ export default {
ipcRenderer.on(
"autofill.listenAutotypeRequest",
(
event,
_event,
data: {
windowTitle: string;
},
@@ -150,10 +151,11 @@ export default {
fn(windowTitle, (error, vaultData) => {
if (error) {
ipcRenderer.send("autofill.completeError", {
const matchError: AutotypeMatchError = {
windowTitle,
error: error.message,
});
errorMessage: error.message,
};
ipcRenderer.send("autofill.completeAutotypeError", matchError);
return;
}
if (vaultData !== null) {

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

@@ -166,8 +166,8 @@ describe("BrowserExtensionPromptComponent", () => {
it("shows manual open error message", () => {
const manualText = fixture.debugElement.query(By.css("p")).nativeElement;
expect(manualText.textContent.trim()).toContain("openExtensionManuallyPart1");
expect(manualText.textContent.trim()).toContain("openExtensionManuallyPart2");
expect(manualText.textContent.trim()).toContain("openExtensionFromToolbarPart1");
expect(manualText.textContent.trim()).toContain("openExtensionFromToolbarPart2");
});
});
});

View File

@@ -1,5 +1,5 @@
import { CommonModule, DOCUMENT } from "@angular/common";
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { Component, Inject, OnDestroy, OnInit, ChangeDetectionStrategy } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute } from "@angular/router";
import { map, Observable, of, tap } from "rxjs";
@@ -14,12 +14,11 @@ import {
} from "../../services/browser-extension-prompt.service";
import { ManuallyOpenExtensionComponent } from "../manually-open-extension/manually-open-extension.component";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
selector: "vault-browser-extension-prompt",
templateUrl: "./browser-extension-prompt.component.html",
imports: [CommonModule, I18nPipe, ButtonComponent, IconModule, ManuallyOpenExtensionComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BrowserExtensionPromptComponent implements OnInit, OnDestroy {
protected VaultMessages = VaultMessages;

View File

@@ -1,8 +1,8 @@
<p bitTypography="body1" class="tw-mb-0 tw-text-xl">
{{ "openExtensionManuallyPart1" | i18n }}
<p bitTypography="body1" class="tw-mb-0">
{{ "openExtensionFromToolbarPart1" | i18n }}
<bit-icon
[icon]="BitwardenIcon"
class="[&>svg]:tw-align-baseline [&>svg]:-tw-mb-[2px]"
class="!tw-inline-block [&>svg]:tw-align-baseline [&>svg]:-tw-mb-[0.25rem]"
></bit-icon>
{{ "openExtensionManuallyPart2" | i18n }}
{{ "openExtensionFromToolbarPart2" | i18n }}
</p>

View File

@@ -1,12 +1,11 @@
import { Component } from "@angular/core";
import { Component, ChangeDetectionStrategy } from "@angular/core";
import { BitwardenIcon } from "@bitwarden/assets/svg";
import { IconModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "vault-manually-open-extension",
templateUrl: "./manually-open-extension.component.html",
imports: [I18nPipe, IconModule],

View File

@@ -29,10 +29,7 @@
</div>
</section>
<section
*ngIf="state === SetupExtensionState.Success || state === SetupExtensionState.AlreadyInstalled"
class="tw-flex tw-flex-col tw-items-center"
>
<section *ngIf="showSuccessUI" class="tw-flex tw-flex-col tw-items-center">
<div class="tw-size-[90px]">
<bit-icon [icon]="PartyIcon"></bit-icon>
</div>
@@ -40,20 +37,15 @@
{{
(state === SetupExtensionState.Success
? "bitwardenExtensionInstalled"
: "openTheBitwardenExtension"
: "bitwardenExtensionIsInstalled"
) | i18n
}}
</h1>
<div
class="tw-flex tw-flex-col tw-rounded-2xl tw-bg-background tw-border tw-border-solid tw-border-secondary-300 tw-p-8 tw-max-w-md tw-text-center"
class="tw-flex tw-flex-col tw-rounded-2xl tw-bg-background tw-border tw-border-solid tw-border-secondary-300 tw-p-8 tw-max-w-md md:tw-w-[28rem] tw-text-center"
>
<p>
{{
(state === SetupExtensionState.Success
? "openExtensionToAutofill"
: "bitwardenExtensionInstalledOpenExtension"
) | i18n
}}
{{ "openExtensionToAutofill" | i18n }}
</p>
<button type="button" bitButton buttonType="primary" class="tw-mb-2" (click)="openExtension()">
{{ "openBitwardenExtension" | i18n }}
@@ -61,8 +53,16 @@
<a bitButton buttonType="secondary" routerLink="/vault">
{{ "skipToWebApp" | i18n }}
</a>
<div
*ngIf="state === SetupExtensionState.ManualOpen"
aria-live="polite"
class="tw-text-center tw-mt-4"
>
<vault-manually-open-extension></vault-manually-open-extension>
</div>
</div>
<p class="tw-mt-10 tw-max-w-96 tw-text-center">
<p class="tw-mt-4 tw-max-w-96 tw-text-center">
{{ "gettingStartedWithBitwardenPart1" | i18n }}
<a bitLink href="https://bitwarden.com/help/learning-center/">
{{ "gettingStartedWithBitwardenPart2" | i18n }}
@@ -73,7 +73,3 @@
</a>
</p>
</section>
<section *ngIf="state === SetupExtensionState.ManualOpen" aria-live="polite" class="tw-text-center">
<vault-manually-open-extension></vault-manually-open-extension>
</section>

View File

@@ -4,7 +4,6 @@ import { Router, RouterModule } from "@angular/router";
import { BehaviorSubject } from "rxjs";
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
import { BrowserExtensionIcon } from "@bitwarden/assets/svg";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { DeviceType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -12,7 +11,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { StateProvider } from "@bitwarden/common/platform/state";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { AnonLayoutWrapperDataService } from "@bitwarden/components";
import { WebBrowserInteractionService } from "../../services/web-browser-interaction.service";
@@ -25,14 +23,12 @@ describe("SetupExtensionComponent", () => {
const navigate = jest.fn().mockResolvedValue(true);
const openExtension = jest.fn().mockResolvedValue(true);
const update = jest.fn().mockResolvedValue(true);
const setAnonLayoutWrapperData = jest.fn();
const extensionInstalled$ = new BehaviorSubject<boolean | null>(null);
beforeEach(async () => {
navigate.mockClear();
openExtension.mockClear();
update.mockClear();
setAnonLayoutWrapperData.mockClear();
window.matchMedia = jest.fn().mockReturnValue(false);
await TestBed.configureTestingModule({
@@ -43,7 +39,6 @@ describe("SetupExtensionComponent", () => {
{ provide: PlatformUtilsService, useValue: { getDevice: () => DeviceType.UnknownBrowser } },
{ provide: SYSTEM_THEME_OBSERVABLE, useValue: new BehaviorSubject("system") },
{ provide: ThemeStateService, useValue: { selectedTheme$: new BehaviorSubject("system") } },
{ provide: AnonLayoutWrapperDataService, useValue: { setAnonLayoutWrapperData } },
{
provide: AccountService,
useValue: { activeAccount$: new BehaviorSubject({ account: { id: "account-id" } }) },
@@ -133,14 +128,6 @@ describe("SetupExtensionComponent", () => {
tick();
expect(component["state"]).toBe(SetupExtensionState.ManualOpen);
expect(setAnonLayoutWrapperData).toHaveBeenCalledWith({
pageTitle: {
key: "somethingWentWrong",
},
pageIcon: BrowserExtensionIcon,
hideCardWrapper: false,
maxWidth: "md",
});
}));
});
});

View File

@@ -5,7 +5,7 @@ import { Router, RouterModule } from "@angular/router";
import { firstValueFrom, pairwise, startWith } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { BrowserExtensionIcon, Party } from "@bitwarden/assets/svg";
import { Party } from "@bitwarden/assets/svg";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -14,7 +14,6 @@ import { StateProvider } from "@bitwarden/common/platform/state";
import { UnionOfValues } from "@bitwarden/common/vault/types/union-of-values";
import { getWebStoreUrl } from "@bitwarden/common/vault/utils/get-web-store-url";
import {
AnonLayoutWrapperDataService,
ButtonComponent,
CenterPositionStrategy,
DialogRef,
@@ -68,7 +67,6 @@ export class SetupExtensionComponent implements OnInit, OnDestroy {
private stateProvider = inject(StateProvider);
private accountService = inject(AccountService);
private document = inject(DOCUMENT);
private anonLayoutWrapperDataService = inject(AnonLayoutWrapperDataService);
protected SetupExtensionState = SetupExtensionState;
protected PartyIcon = Party;
@@ -144,6 +142,16 @@ export class SetupExtensionComponent implements OnInit, OnDestroy {
}
}
get showSuccessUI(): boolean {
const successStates = [
SetupExtensionState.Success,
SetupExtensionState.AlreadyInstalled,
SetupExtensionState.ManualOpen,
] as string[];
return successStates.includes(this.state);
}
/** Opens the add extension later dialog */
addItLater() {
this.dialogRef = this.dialogService.open<unknown, AddExtensionLaterDialogData>(
@@ -161,16 +169,6 @@ export class SetupExtensionComponent implements OnInit, OnDestroy {
async openExtension() {
await this.webBrowserExtensionInteractionService.openExtension().catch(() => {
this.state = SetupExtensionState.ManualOpen;
// Update the anon layout data to show the proper error design
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: {
key: "somethingWentWrong",
},
pageIcon: BrowserExtensionIcon,
hideCardWrapper: false,
maxWidth: "md",
});
});
}

View File

@@ -25,7 +25,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
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";
@@ -181,13 +180,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
message: this.i18nService.t("unlinkedSso"),
});
const disableAlternateLoginMethodsFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM22110_DisableAlternateLoginMethods,
);
if (disableAlternateLoginMethodsFlagEnabled) {
await this.removeEmailFromSsoRequiredCacheIfPresent();
}
await this.removeEmailFromSsoRequiredCacheIfPresent();
} catch (e) {
this.logService.error(e);
}
@@ -214,13 +207,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy {
message: this.i18nService.t("leftOrganization"),
});
const disableAlternateLoginMethodsFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM22110_DisableAlternateLoginMethods,
);
if (disableAlternateLoginMethodsFlagEnabled) {
await this.removeEmailFromSsoRequiredCacheIfPresent();
}
await this.removeEmailFromSsoRequiredCacheIfPresent();
} catch (e) {
this.logService.error(e);
}

View File

@@ -11351,14 +11351,6 @@
"openedExtensionViewAtRiskPasswords": {
"message": "Successfully opened the Bitwarden browser extension. You can now review your at-risk passwords."
},
"openExtensionManuallyPart1": {
"message": "We had trouble opening the Bitwarden browser extension. Open the Bitwarden icon",
"description": "This will be used as part of a larger sentence, broken up to include the Bitwarden icon. The full sentence will read 'We had trouble opening the Bitwarden browser extension. Open the Bitwarden icon [Bitwarden Icon] from the toolbar.'"
},
"openExtensionManuallyPart2": {
"message": "from the toolbar.",
"description": "This will be used as part of a larger sentence, broken up to include the Bitwarden icon. The full sentence will read 'We had trouble opening the Bitwarden browser extension. Open the Bitwarden icon [Bitwarden Icon] from the toolbar.'"
},
"resellerRenewalWarningMsg": {
"message": "Your subscription will renew soon. To ensure uninterrupted service, contact $RESELLER$ to confirm your renewal before $RENEWAL_DATE$.",
"placeholders": {
@@ -11679,11 +11671,8 @@
"bitwardenExtensionInstalled": {
"message": "Bitwarden extension installed!"
},
"openTheBitwardenExtension": {
"message": "Open the Bitwarden extension"
},
"bitwardenExtensionInstalledOpenExtension": {
"message": "The Bitwarden extension is installed! Open the extension to log in and start autofilling."
"bitwardenExtensionIsInstalled": {
"message": "Bitwarden extension is installed!"
},
"openExtensionToAutofill": {
"message": "Open the extension to log in and start autofilling."
@@ -11699,6 +11688,14 @@
"message": "Learning Center",
"description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'"
},
"openExtensionFromToolbarPart1": {
"message": "If the extension didn't open, you may need to open Bitwarden from the icon ",
"description": "This will be used as part of a larger sentence, broken up to include the Bitwarden icon. The full sentence will read 'If the extension didn't open, you may need to open Bitwarden from the icon [Bitwarden Icon] on the toolbar.'"
},
"openExtensionFromToolbarPart2": {
"message": " on the toolbar.",
"description": "This will be used as part of a larger sentence, broken up to include the Bitwarden icon. The full sentence will read 'If the extension didn't open, you may need to open Bitwarden from the icon [Bitwarden Icon] on the toolbar.'"
},
"gettingStartedWithBitwardenPart3": {
"message": "Help Center",
"description": "This will be displayed as part of a larger sentence. The whole sentence reads: 'For tips on getting started with Bitwarden visit the Learning Center and Help Center'"
@@ -12178,5 +12175,8 @@
},
"confirmNoSelectedCriticalApplicationsDesc": {
"message": "Are you sure you want to continue?"
},
"userVerificationFailed": {
"message": "User verification failed."
}
}

View File

@@ -28,7 +28,7 @@
</div>
<dirt-review-applications-view
[applications]="getApplications()"
[applications]="applicationsWithIcons()"
[selectedApplications]="selectedApplications()"
(onToggleSelection)="toggleSelection($event)"
(onToggleAll)="toggleAll()"

View File

@@ -33,6 +33,7 @@ import {
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { CipherIcon } from "../../shared/app-table-row-scrollable.component";
import { AccessIntelligenceSecurityTasksService } from "../../shared/security-tasks.service";
import { AssignTasksViewComponent } from "./assign-tasks-view.component";
@@ -99,6 +100,16 @@ export class NewApplicationsDialogComponent {
// Applications selected to save as critical applications
protected readonly selectedApplications = signal<Set<string>>(new Set());
protected readonly applicationIcons = signal<Map<string, CipherIcon>>(
new Map<string, CipherIcon>(),
);
protected readonly applicationsWithIcons = computed(() => {
return this.dialogParams.newApplications.map((app) => {
const iconCipher = this.applicationIcons().get(app.applicationName);
return { ...app, iconCipher } as ApplicationHealthReportDetail & { iconCipher: CipherIcon };
});
});
// Used to determine if there are unassigned at-risk cipher IDs
private readonly _tasks!: Signal<SecurityTask[]>;
@@ -150,6 +161,7 @@ export class NewApplicationsDialogComponent {
private securityTasksService: AccessIntelligenceSecurityTasksService,
private toastService: ToastService,
) {
this.setApplicationIconMap(this.dialogParams.newApplications);
// Setup the _tasks signal by manually passing in the injector
this._tasks = toSignal(this.securityTasksService.tasks$, {
initialValue: [],
@@ -172,10 +184,6 @@ export class NewApplicationsDialogComponent {
);
}
getApplications() {
return this.dialogParams.newApplications;
}
/**
* Returns true if the organization has no existing critical applications.
* Used to conditionally show different titles and descriptions.
@@ -184,6 +192,22 @@ export class NewApplicationsDialogComponent {
return !this.dialogParams.hasExistingCriticalApplications;
}
/**
* Maps applications to a corresponding iconCipher
*
* @param applications
*/
setApplicationIconMap(applications: ApplicationHealthReportDetail[]) {
// Map the report data to include the iconCipher for each application
const iconCiphers = new Map<string, CipherIcon>();
applications.forEach((app) => {
const iconCipher =
app.cipherIds.length > 0 ? this.dataService.getCipherIcon(app.cipherIds[0]) : undefined;
iconCiphers.set(app.applicationName, iconCipher);
});
this.applicationIcons.set(iconCiphers);
}
/**
* Toggles the selection state of an application.
* @param applicationName The application to toggle

View File

@@ -62,7 +62,11 @@
</td>
<td bitTypography="body1" class="tw-py-3 tw-px-2">
<div class="tw-flex tw-items-center tw-gap-2">
<i class="bwi bwi-globe tw-text-muted" aria-hidden="true"></i>
@if (app.iconCipher) {
<app-vault-icon [cipher]="app.iconCipher"></app-vault-icon>
} @else {
<i class="bwi bwi-globe tw-text-muted" aria-hidden="true"></i>
}
<span>{{ app.applicationName }}</span>
</div>
</td>

View File

@@ -5,6 +5,9 @@ import { FormsModule } from "@angular/forms";
import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/dirt/reports/risk-insights";
import { ButtonModule, DialogModule, SearchModule, TypographyModule } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { CipherIcon } from "../../shared/app-table-row-scrollable.component";
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -18,10 +21,12 @@ import { I18nPipe } from "@bitwarden/ui-common";
SearchModule,
TypographyModule,
I18nPipe,
SharedModule,
],
})
export class ReviewApplicationsViewComponent {
readonly applications = input.required<ApplicationHealthReportDetail[]>();
readonly applications =
input.required<Array<ApplicationHealthReportDetail & { iconCipher: CipherIcon }>>();
readonly selectedApplications = input.required<Set<string>>();
protected readonly searchText = signal<string>("");

View File

@@ -45,7 +45,6 @@
tabindex="0"
[attr.aria-label]="'viewItem' | i18n"
>
<!-- Passing the first cipher of the application for app-vault-icon cipher input requirement -->
<app-vault-icon *ngIf="row.iconCipher" [cipher]="row.iconCipher"></app-vault-icon>
</td>
<td

View File

@@ -8,8 +8,10 @@ import { MenuModule, TableDataSource, TableModule } from "@bitwarden/components"
import { SharedModule } from "@bitwarden/web-vault/app/shared";
import { PipesModule } from "@bitwarden/web-vault/app/vault/individual-vault/pipes/pipes.module";
export type CipherIcon = CipherViewLike | undefined;
export type ApplicationTableDataSource = ApplicationHealthReportDetailEnriched & {
iconCipher: CipherViewLike | undefined;
iconCipher: CipherIcon;
};
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush

View File

@@ -102,7 +102,6 @@ import { MasterPasswordApiService as MasterPasswordApiServiceAbstraction } from
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";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { WebAuthnLoginApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-api.service.abstraction";
@@ -125,13 +124,17 @@ import { OrganizationInviteService } from "@bitwarden/common/auth/services/organ
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";
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 { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-api.service";
import { WebAuthnLoginPrfKeyService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-key.service";
import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service";
import { TwoFactorApiService, DefaultTwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import {
TwoFactorApiService,
DefaultTwoFactorApiService,
TwoFactorService,
DefaultTwoFactorService,
} from "@bitwarden/common/auth/two-factor";
import {
AutofillSettingsService,
AutofillSettingsServiceAbstraction,
@@ -527,7 +530,7 @@ const safeProviders: SafeProvider[] = [
KeyConnectorServiceAbstraction,
EnvironmentService,
StateServiceAbstraction,
TwoFactorServiceAbstraction,
TwoFactorService,
I18nServiceAbstraction,
EncryptService,
PasswordStrengthServiceAbstraction,
@@ -1165,9 +1168,14 @@ const safeProviders: SafeProvider[] = [
deps: [StateProvider],
}),
safeProvider({
provide: TwoFactorServiceAbstraction,
useClass: TwoFactorService,
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction, GlobalStateProvider],
provide: TwoFactorService,
useClass: DefaultTwoFactorService,
deps: [
I18nServiceAbstraction,
PlatformUtilsServiceAbstraction,
GlobalStateProvider,
TwoFactorApiService,
],
}),
safeProvider({
provide: FormValidationErrorsServiceAbstraction,

View File

@@ -205,14 +205,9 @@ export class LoginComponent implements OnInit, OnDestroy {
await this.loadRememberedEmail();
}
const disableAlternateLoginMethodsFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM22110_DisableAlternateLoginMethods,
);
if (disableAlternateLoginMethodsFlagEnabled) {
// This SSO required check should come after email has had a chance to be pre-filled (if it
// was found in query params or was the remembered email)
await this.determineIfSsoRequired();
}
// This SSO required check should come after email has had a chance to be pre-filled (if it
// was found in query params or was the remembered email)
await this.determineIfSsoRequired();
}
private async desktopOnInit(): Promise<void> {

View File

@@ -4,10 +4,9 @@ import { ReactiveFormsModule, FormsModule, FormControl } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { TwoFactorApiService } from "@bitwarden/common/auth/two-factor";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -68,7 +67,6 @@ export class TwoFactorAuthEmailComponent implements OnInit {
protected loginStrategyService: LoginStrategyServiceAbstraction,
protected platformUtilsService: PlatformUtilsService,
protected logService: LogService,
protected twoFactorApiService: TwoFactorApiService,
protected appIdService: AppIdService,
private toastService: ToastService,
private cacheService: TwoFactorAuthEmailComponentCacheService,
@@ -137,7 +135,7 @@ export class TwoFactorAuthEmailComponent implements OnInit {
request.deviceIdentifier = await this.appIdService.getAppId();
request.authRequestAccessCode = (await this.loginStrategyService.getAccessCode()) ?? "";
request.authRequestId = (await this.loginStrategyService.getAuthRequestId()) ?? "";
this.emailPromise = this.twoFactorApiService.postTwoFactorEmail(request);
this.emailPromise = this.twoFactorService.postTwoFactorEmail(request);
await this.emailPromise;
this.emailSent = true;

View File

@@ -6,8 +6,8 @@ import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { WebAuthnIFrame } from "@bitwarden/common/auth/webauthn-iframe";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";

View File

@@ -18,12 +18,12 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
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 { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service";
import {
InternalMasterPasswordServiceAbstraction,

View File

@@ -32,12 +32,12 @@ import {
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
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 { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";

View File

@@ -4,8 +4,8 @@ import { provideRouter, Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { LoginStrategyServiceAbstraction } from "../../common";

View File

@@ -8,7 +8,7 @@ import {
} from "@angular/router";
import { firstValueFrom } from "rxjs";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { LoginStrategyServiceAbstraction } from "../../common";

View File

@@ -9,11 +9,8 @@ import {
TwoFactorAuthWebAuthnIcon,
TwoFactorAuthYubicoIcon,
} from "@bitwarden/assets/svg";
import {
TwoFactorProviderDetails,
TwoFactorService,
} from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorProviderDetails, TwoFactorService } from "@bitwarden/common/auth/two-factor";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import {

View File

@@ -277,13 +277,13 @@ export class UserVerificationDialogComponent {
});
}
}
} catch (e) {
} catch {
// Catch handles OTP and MP verification scenarios as those throw errors on verification failure instead of returning false like PIN and biometrics.
this.invalidSecret = true;
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("error"),
message: e.message,
message: this.i18nService.t("userVerificationFailed"),
});
return;
}

View File

@@ -3,8 +3,8 @@ import { BehaviorSubject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { TwoFactorService } from "@bitwarden/common/auth/two-factor";
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";

View File

@@ -5,7 +5,6 @@ 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 { 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";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@@ -16,6 +15,7 @@ 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 { TwoFactorService } from "@bitwarden/common/auth/two-factor";
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 { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";

View File

@@ -3,7 +3,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 { 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";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
@@ -16,6 +15,7 @@ import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";

View File

@@ -5,12 +5,12 @@ 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 { 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";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
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 { TwoFactorService } from "@bitwarden/common/auth/two-factor";
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";

View File

@@ -3,12 +3,12 @@ import { BehaviorSubject, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.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 { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
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 { TwoFactorService } from "@bitwarden/common/auth/two-factor";
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 { EncryptedString } from "@bitwarden/common/key-management/crypto/models/enc-string";

View File

@@ -3,7 +3,7 @@ import { BehaviorSubject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
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";

View File

@@ -3,11 +3,11 @@ import { BehaviorSubject } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
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 { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
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";

View File

@@ -4,13 +4,13 @@ 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 { 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";
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 { 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 { TwoFactorService } from "@bitwarden/common/auth/two-factor";
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";

View File

@@ -13,10 +13,10 @@ 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 { 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";
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 { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";

View File

@@ -1,7 +1,6 @@
import { MockProxy, mock } from "jest-mock-extended";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
@@ -58,62 +57,35 @@ describe("DefaultLoginSuccessHandlerService", () => {
expect(loginEmailService.clearLoginEmail).toHaveBeenCalled();
});
describe("when PM22110_DisableAlternateLoginMethods flag is disabled", () => {
it("should get SSO email", async () => {
await service.run(userId);
expect(ssoLoginService.getSsoEmail).toHaveBeenCalled();
});
describe("given SSO email is not found", () => {
beforeEach(() => {
configService.getFeatureFlag.mockResolvedValue(false);
ssoLoginService.getSsoEmail.mockResolvedValue(null);
});
it("should not check SSO requirements", async () => {
it("should log error and return early", async () => {
await service.run(userId);
expect(ssoLoginService.getSsoEmail).not.toHaveBeenCalled();
expect(logService.error).toHaveBeenCalledWith("SSO login email not found.");
expect(ssoLoginService.updateSsoRequiredCache).not.toHaveBeenCalled();
});
});
describe("given PM22110_DisableAlternateLoginMethods flag is enabled", () => {
describe("given SSO email is found", () => {
beforeEach(() => {
configService.getFeatureFlag.mockResolvedValue(true);
ssoLoginService.getSsoEmail.mockResolvedValue(testEmail);
});
it("should check feature flag", async () => {
it("should call updateSsoRequiredCache() and clearSsoEmail()", async () => {
await service.run(userId);
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
FeatureFlag.PM22110_DisableAlternateLoginMethods,
);
});
it("should get SSO email", async () => {
await service.run(userId);
expect(ssoLoginService.getSsoEmail).toHaveBeenCalled();
});
describe("given SSO email is not found", () => {
beforeEach(() => {
ssoLoginService.getSsoEmail.mockResolvedValue(null);
});
it("should log error and return early", async () => {
await service.run(userId);
expect(logService.error).toHaveBeenCalledWith("SSO login email not found.");
expect(ssoLoginService.updateSsoRequiredCache).not.toHaveBeenCalled();
});
});
describe("given SSO email is found", () => {
beforeEach(() => {
ssoLoginService.getSsoEmail.mockResolvedValue(testEmail);
});
it("should call updateSsoRequiredCache() and clearSsoEmail()", async () => {
await service.run(userId);
expect(ssoLoginService.updateSsoRequiredCache).toHaveBeenCalledWith(testEmail, userId);
expect(ssoLoginService.clearSsoEmail).toHaveBeenCalled();
});
expect(ssoLoginService.updateSsoRequiredCache).toHaveBeenCalledWith(testEmail, userId);
expect(ssoLoginService.clearSsoEmail).toHaveBeenCalled();
});
});
});

View File

@@ -1,5 +1,4 @@
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { UserId } from "@bitwarden/common/types/guid";
@@ -23,20 +22,14 @@ export class DefaultLoginSuccessHandlerService implements LoginSuccessHandlerSer
await this.userAsymmetricKeysRegenerationService.regenerateIfNeeded(userId);
await this.loginEmailService.clearLoginEmail();
const disableAlternateLoginMethodsFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM22110_DisableAlternateLoginMethods,
);
const ssoLoginEmail = await this.ssoLoginService.getSsoEmail();
if (disableAlternateLoginMethodsFlagEnabled) {
const ssoLoginEmail = await this.ssoLoginService.getSsoEmail();
if (!ssoLoginEmail) {
this.logService.error("SSO login email not found.");
return;
}
await this.ssoLoginService.updateSsoRequiredCache(ssoLoginEmail, userId);
await this.ssoLoginService.clearSsoEmail();
if (!ssoLoginEmail) {
this.logService.error("SSO login email not found.");
return;
}
await this.ssoLoginService.updateSsoRequiredCache(ssoLoginEmail, userId);
await this.ssoLoginService.clearSsoEmail();
}
}

View File

@@ -1,60 +0,0 @@
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
export interface TwoFactorProviderDetails {
type: TwoFactorProviderType;
name: string;
description: string;
priority: number;
sort: number;
premium: boolean;
}
export abstract class TwoFactorService {
/**
* Initializes the client-side's TwoFactorProviders const with translations.
*/
abstract init(): void;
/**
* Gets a list of two-factor providers from state that are supported on the current client.
* E.g., WebAuthn and Duo are not available on all clients.
* @returns A list of supported two-factor providers or an empty list if none are stored in state.
*/
abstract getSupportedProviders(win: Window): Promise<TwoFactorProviderDetails[]>;
/**
* Gets the previously selected two-factor provider or the default two factor provider based on priority.
* @param webAuthnSupported - Whether or not WebAuthn is supported by the client. Prevents WebAuthn from being the default provider if false.
*/
abstract getDefaultProvider(webAuthnSupported: boolean): Promise<TwoFactorProviderType>;
/**
* Sets the selected two-factor provider in state.
* @param type - The type of two-factor provider to set as the selected provider.
*/
abstract setSelectedProvider(type: TwoFactorProviderType): Promise<void>;
/**
* Clears the selected two-factor provider from state.
*/
abstract clearSelectedProvider(): Promise<void>;
/**
* Sets the list of available two-factor providers in state.
* @param response - the response from Identity for when 2FA is required. Includes the list of available 2FA providers.
*/
abstract setProviders(response: IdentityTwoFactorResponse): Promise<void>;
/**
* Clears the list of available two-factor providers from state.
*/
abstract clearProviders(): Promise<void>;
/**
* Gets the list of two-factor providers from state.
* Note: no filtering is done here, so this will return all providers, including potentially
* unsupported ones for the current client.
* @returns A list of two-factor providers or null if none are stored in state.
*/
abstract getProviders(): Promise<Map<TwoFactorProviderType, { [key: string]: string }> | null>;
}

View File

@@ -1,212 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, map } from "rxjs";
import { I18nService } from "../../platform/abstractions/i18n.service";
import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service";
import { Utils } from "../../platform/misc/utils";
import { GlobalStateProvider, KeyDefinition, TWO_FACTOR_MEMORY } from "../../platform/state";
import {
TwoFactorProviderDetails,
TwoFactorService as TwoFactorServiceAbstraction,
} from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
export const TwoFactorProviders: Partial<Record<TwoFactorProviderType, TwoFactorProviderDetails>> =
{
[TwoFactorProviderType.Authenticator]: {
type: TwoFactorProviderType.Authenticator,
name: null as string,
description: null as string,
priority: 1,
sort: 2,
premium: false,
},
[TwoFactorProviderType.Yubikey]: {
type: TwoFactorProviderType.Yubikey,
name: null as string,
description: null as string,
priority: 3,
sort: 4,
premium: true,
},
[TwoFactorProviderType.Duo]: {
type: TwoFactorProviderType.Duo,
name: "Duo",
description: null as string,
priority: 2,
sort: 5,
premium: true,
},
[TwoFactorProviderType.OrganizationDuo]: {
type: TwoFactorProviderType.OrganizationDuo,
name: "Duo (Organization)",
description: null as string,
priority: 10,
sort: 6,
premium: false,
},
[TwoFactorProviderType.Email]: {
type: TwoFactorProviderType.Email,
name: null as string,
description: null as string,
priority: 0,
sort: 1,
premium: false,
},
[TwoFactorProviderType.WebAuthn]: {
type: TwoFactorProviderType.WebAuthn,
name: null as string,
description: null as string,
priority: 4,
sort: 3,
premium: false,
},
};
// Memory storage as only required during authentication process
export const PROVIDERS = KeyDefinition.record<Record<string, string>, TwoFactorProviderType>(
TWO_FACTOR_MEMORY,
"providers",
{
deserializer: (obj) => obj,
},
);
// Memory storage as only required during authentication process
export const SELECTED_PROVIDER = new KeyDefinition<TwoFactorProviderType>(
TWO_FACTOR_MEMORY,
"selected",
{
deserializer: (obj) => obj,
},
);
export class TwoFactorService implements TwoFactorServiceAbstraction {
private providersState = this.globalStateProvider.get(PROVIDERS);
private selectedState = this.globalStateProvider.get(SELECTED_PROVIDER);
readonly providers$ = this.providersState.state$.pipe(
map((providers) => Utils.recordToMap(providers)),
);
readonly selected$ = this.selectedState.state$;
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private globalStateProvider: GlobalStateProvider,
) {}
init() {
TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t("emailTitle");
TwoFactorProviders[TwoFactorProviderType.Email].description = this.i18nService.t("emailDescV2");
TwoFactorProviders[TwoFactorProviderType.Authenticator].name =
this.i18nService.t("authenticatorAppTitle");
TwoFactorProviders[TwoFactorProviderType.Authenticator].description =
this.i18nService.t("authenticatorAppDescV2");
TwoFactorProviders[TwoFactorProviderType.Duo].description = this.i18nService.t("duoDescV2");
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].name =
"Duo (" + this.i18nService.t("organization") + ")";
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].description =
this.i18nService.t("duoOrganizationDesc");
TwoFactorProviders[TwoFactorProviderType.WebAuthn].name = this.i18nService.t("webAuthnTitle");
TwoFactorProviders[TwoFactorProviderType.WebAuthn].description =
this.i18nService.t("webAuthnDesc");
TwoFactorProviders[TwoFactorProviderType.Yubikey].name = this.i18nService.t("yubiKeyTitleV2");
TwoFactorProviders[TwoFactorProviderType.Yubikey].description =
this.i18nService.t("yubiKeyDesc");
}
async getSupportedProviders(win: Window): Promise<TwoFactorProviderDetails[]> {
const data = await firstValueFrom(this.providers$);
const providers: any[] = [];
if (data == null) {
return providers;
}
if (
data.has(TwoFactorProviderType.OrganizationDuo) &&
this.platformUtilsService.supportsDuo()
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]);
}
if (data.has(TwoFactorProviderType.Authenticator)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]);
}
if (data.has(TwoFactorProviderType.Yubikey)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]);
}
if (data.has(TwoFactorProviderType.Duo) && this.platformUtilsService.supportsDuo()) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]);
}
if (
data.has(TwoFactorProviderType.WebAuthn) &&
this.platformUtilsService.supportsWebAuthn(win)
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]);
}
if (data.has(TwoFactorProviderType.Email)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Email]);
}
return providers;
}
async getDefaultProvider(webAuthnSupported: boolean): Promise<TwoFactorProviderType> {
const data = await firstValueFrom(this.providers$);
const selected = await firstValueFrom(this.selected$);
if (data == null) {
return null;
}
if (selected != null && data.has(selected)) {
return selected;
}
let providerType: TwoFactorProviderType = null;
let providerPriority = -1;
data.forEach((_value, type) => {
const provider = (TwoFactorProviders as any)[type];
if (provider != null && provider.priority > providerPriority) {
if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) {
return;
}
providerType = type;
providerPriority = provider.priority;
}
});
return providerType;
}
async setSelectedProvider(type: TwoFactorProviderType): Promise<void> {
await this.selectedState.update(() => type);
}
async clearSelectedProvider(): Promise<void> {
await this.selectedState.update(() => null);
}
async setProviders(response: IdentityTwoFactorResponse): Promise<void> {
await this.providersState.update(() => response.twoFactorProviders2);
}
async clearProviders(): Promise<void> {
await this.providersState.update(() => null);
}
getProviders(): Promise<Map<TwoFactorProviderType, { [key: string]: string }> | null> {
return firstValueFrom(this.providers$);
}
}

View File

@@ -0,0 +1,2 @@
export * from "./two-factor-api.service";
export * from "./two-factor.service";

View File

@@ -0,0 +1,497 @@
import { ListResponse } from "../../../models/response/list.response";
import { KeyDefinition, TWO_FACTOR_MEMORY } from "../../../platform/state";
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
import { DisableTwoFactorAuthenticatorRequest } from "../../models/request/disable-two-factor-authenticator.request";
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
import { TwoFactorEmailRequest } from "../../models/request/two-factor-email.request";
import { TwoFactorProviderRequest } from "../../models/request/two-factor-provider.request";
import { UpdateTwoFactorAuthenticatorRequest } from "../../models/request/update-two-factor-authenticator.request";
import { UpdateTwoFactorDuoRequest } from "../../models/request/update-two-factor-duo.request";
import { UpdateTwoFactorEmailRequest } from "../../models/request/update-two-factor-email.request";
import { UpdateTwoFactorWebAuthnDeleteRequest } from "../../models/request/update-two-factor-web-authn-delete.request";
import { UpdateTwoFactorWebAuthnRequest } from "../../models/request/update-two-factor-web-authn.request";
import { UpdateTwoFactorYubikeyOtpRequest } from "../../models/request/update-two-factor-yubikey-otp.request";
import { IdentityTwoFactorResponse } from "../../models/response/identity-two-factor.response";
import { TwoFactorAuthenticatorResponse } from "../../models/response/two-factor-authenticator.response";
import { TwoFactorDuoResponse } from "../../models/response/two-factor-duo.response";
import { TwoFactorEmailResponse } from "../../models/response/two-factor-email.response";
import { TwoFactorProviderResponse } from "../../models/response/two-factor-provider.response";
import { TwoFactorRecoverResponse } from "../../models/response/two-factor-recover.response";
import {
ChallengeResponse,
TwoFactorWebAuthnResponse,
} from "../../models/response/two-factor-web-authn.response";
import { TwoFactorYubiKeyResponse } from "../../models/response/two-factor-yubi-key.response";
/**
* Metadata and display information for a two-factor authentication provider.
* Used by UI components to render provider selection and configuration screens.
*/
export interface TwoFactorProviderDetails {
/** The unique identifier for this provider type. */
type: TwoFactorProviderType;
/**
* Display name for the provider, localized via {@link TwoFactorService.init}.
* Examples: "Authenticator App", "Email", "YubiKey".
*/
name: string | null;
/**
* User-facing description explaining what this provider is and how it works.
* Localized via {@link TwoFactorService.init}.
*/
description: string | null;
/**
* Selection priority during login when multiple providers are available.
* Higher values are preferred. Used to determine the default provider.
* Range: 0 (lowest) to 10 (highest).
*/
priority: number;
/**
* Display order in provider lists within settings UI.
* Lower values appear first (1 = first position).
*/
sort: number;
/**
* Whether this provider requires an active premium subscription.
* Premium providers: Duo (personal), YubiKey.
* Organization providers (e.g., OrganizationDuo) do not require personal premium.
*/
premium: boolean;
}
/**
* Registry of all supported two-factor authentication providers with their metadata.
* Strings (name, description) are initialized as null and populated with localized
* translations when {@link TwoFactorService.init} is called during application startup.
*
* @remarks
* This constant is mutated during initialization. Components should not access it before
* the service's init() method has been called.
*
* @example
* ```typescript
* // During app init
* twoFactorService.init();
*
* // In components
* const authenticator = TwoFactorProviders[TwoFactorProviderType.Authenticator];
* console.log(authenticator.name); // "Authenticator App" (localized)
* ```
*/
export const TwoFactorProviders: Partial<Record<TwoFactorProviderType, TwoFactorProviderDetails>> =
{
[TwoFactorProviderType.Authenticator]: {
type: TwoFactorProviderType.Authenticator,
name: null,
description: null,
priority: 1,
sort: 2,
premium: false,
},
[TwoFactorProviderType.Yubikey]: {
type: TwoFactorProviderType.Yubikey,
name: null,
description: null,
priority: 3,
sort: 4,
premium: true,
},
[TwoFactorProviderType.Duo]: {
type: TwoFactorProviderType.Duo,
name: "Duo",
description: null,
priority: 2,
sort: 5,
premium: true,
},
[TwoFactorProviderType.OrganizationDuo]: {
type: TwoFactorProviderType.OrganizationDuo,
name: "Duo (Organization)",
description: null,
priority: 10,
sort: 6,
premium: false,
},
[TwoFactorProviderType.Email]: {
type: TwoFactorProviderType.Email,
name: null,
description: null,
priority: 0,
sort: 1,
premium: false,
},
[TwoFactorProviderType.WebAuthn]: {
type: TwoFactorProviderType.WebAuthn,
name: null,
description: null,
priority: 4,
sort: 3,
premium: false,
},
};
// Memory storage as only required during authentication process
export const PROVIDERS = KeyDefinition.record<Record<string, string>, TwoFactorProviderType>(
TWO_FACTOR_MEMORY,
"providers",
{
deserializer: (obj) => obj,
},
);
// Memory storage as only required during authentication process
export const SELECTED_PROVIDER = new KeyDefinition<TwoFactorProviderType>(
TWO_FACTOR_MEMORY,
"selected",
{
deserializer: (obj) => obj,
},
);
export abstract class TwoFactorService {
/**
* Initializes the client-side's TwoFactorProviders const with translations.
*/
abstract init(): void;
/**
* Gets a list of two-factor providers from state that are supported on the current client.
* E.g., WebAuthn and Duo are not available on all clients.
* @returns A list of supported two-factor providers or an empty list if none are stored in state.
*/
abstract getSupportedProviders(win: Window): Promise<TwoFactorProviderDetails[]>;
/**
* Gets the previously selected two-factor provider or the default two factor provider based on priority.
* @param webAuthnSupported - Whether or not WebAuthn is supported by the client. Prevents WebAuthn from being the default provider if false.
*/
abstract getDefaultProvider(webAuthnSupported: boolean): Promise<TwoFactorProviderType>;
/**
* Sets the selected two-factor provider in state.
* @param type - The type of two-factor provider to set as the selected provider.
*/
abstract setSelectedProvider(type: TwoFactorProviderType): Promise<void>;
/**
* Clears the selected two-factor provider from state.
*/
abstract clearSelectedProvider(): Promise<void>;
/**
* Sets the list of available two-factor providers in state.
* @param response - the response from Identity for when 2FA is required. Includes the list of available 2FA providers.
*/
abstract setProviders(response: IdentityTwoFactorResponse): Promise<void>;
/**
* Clears the list of available two-factor providers from state.
*/
abstract clearProviders(): Promise<void>;
/**
* Gets the list of two-factor providers from state.
* Note: no filtering is done here, so this will return all providers, including potentially
* unsupported ones for the current client.
* @returns A list of two-factor providers or null if none are stored in state.
*/
abstract getProviders(): Promise<Map<TwoFactorProviderType, { [key: string]: string }> | null>;
/**
* Gets the enabled two-factor providers for the current user from the API.
* Used for settings management.
* @returns A promise that resolves to a list response containing enabled two-factor provider configurations.
*/
abstract getEnabledTwoFactorProviders(): Promise<ListResponse<TwoFactorProviderResponse>>;
/**
* Gets the enabled two-factor providers for an organization from the API.
* Requires organization administrator permissions.
* Used for settings management.
*
* @param organizationId The ID of the organization.
* @returns A promise that resolves to a list response containing enabled two-factor provider configurations.
*/
abstract getTwoFactorOrganizationProviders(
organizationId: string,
): Promise<ListResponse<TwoFactorProviderResponse>>;
/**
* Gets the authenticator (TOTP) two-factor configuration for the current user from the API.
* Requires user verification via master password or OTP.
* Used for settings management.
*
* @param request The {@link SecretVerificationRequest} to prove authentication.
* @returns A promise that resolves to the authenticator configuration including the secret key.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract getTwoFactorAuthenticator(
request: SecretVerificationRequest,
): Promise<TwoFactorAuthenticatorResponse>;
/**
* Gets the email two-factor configuration for the current user from the API.
* Requires user verification via master password or OTP.
* Used for settings management.
*
* @param request The {@link SecretVerificationRequest} to prove authentication.
* @returns A promise that resolves to the email two-factor configuration.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract getTwoFactorEmail(request: SecretVerificationRequest): Promise<TwoFactorEmailResponse>;
/**
* Gets the Duo two-factor configuration for the current user from the API.
* Requires user verification and an active premium subscription.
* Used for settings management.
*
* @param request The {@link SecretVerificationRequest} to prove authentication.
* @returns A promise that resolves to the Duo configuration.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract getTwoFactorDuo(request: SecretVerificationRequest): Promise<TwoFactorDuoResponse>;
/**
* Gets the Duo two-factor configuration for an organization from the API.
* Requires user verification and organization policy management permissions.
* Used for settings management.
*
* @param organizationId The ID of the organization.
* @param request The {@link SecretVerificationRequest} to prove authentication.
* @returns A promise that resolves to the organization Duo configuration.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract getTwoFactorOrganizationDuo(
organizationId: string,
request: SecretVerificationRequest,
): Promise<TwoFactorDuoResponse>;
/**
* Gets the YubiKey OTP two-factor configuration for the current user from the API.
* Requires user verification and an active premium subscription.
* Used for settings management.
*
* @param request The {@link SecretVerificationRequest} to prove authentication.
* @returns A promise that resolves to the YubiKey configuration.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract getTwoFactorYubiKey(
request: SecretVerificationRequest,
): Promise<TwoFactorYubiKeyResponse>;
/**
* Gets the WebAuthn (FIDO2) two-factor configuration for the current user from the API.
* Requires user verification via master password or OTP.
* Used for settings management.
*
* @param request The {@link SecretVerificationRequest} to authentication.
* @returns A promise that resolves to the WebAuthn configuration including registered credentials.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract getTwoFactorWebAuthn(
request: SecretVerificationRequest,
): Promise<TwoFactorWebAuthnResponse>;
/**
* Gets a WebAuthn challenge for registering a new WebAuthn credential from the API.
* This must be called before putTwoFactorWebAuthn to obtain the cryptographic challenge
* required for credential creation. The challenge is used by the browser's WebAuthn API.
* Requires user verification via master password or OTP.
* Used for settings management.
*
* @param request The {@link SecretVerificationRequest} to prove authentication.
* @returns A promise that resolves to the credential creation options containing the challenge.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract getTwoFactorWebAuthnChallenge(
request: SecretVerificationRequest,
): Promise<ChallengeResponse>;
/**
* Gets the recovery code configuration for the current user from the API.
* The recovery code should be stored securely by the user.
* Requires user verification via master password or OTP.
* Used for settings management.
*
* @param verification The verification information to prove authentication.
* @returns A promise that resolves to the recovery code configuration.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract getTwoFactorRecover(
request: SecretVerificationRequest,
): Promise<TwoFactorRecoverResponse>;
/**
* Enables or updates the authenticator (TOTP) two-factor provider.
* Validates the provided token against the shared secret before enabling.
* The token must be generated by an authenticator app using the secret key.
* Used for settings management.
*
* @param request The {@link UpdateTwoFactorAuthenticatorRequest} to prove authentication.
* @returns A promise that resolves to the updated authenticator configuration.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract putTwoFactorAuthenticator(
request: UpdateTwoFactorAuthenticatorRequest,
): Promise<TwoFactorAuthenticatorResponse>;
/**
* Disables the authenticator (TOTP) two-factor provider for the current user.
* Requires user verification token to confirm the operation.
* Used for settings management.
*
* @param request The {@link DisableTwoFactorAuthenticatorRequest} to prove authentication.
* @returns A promise that resolves to the updated provider status.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract deleteTwoFactorAuthenticator(
request: DisableTwoFactorAuthenticatorRequest,
): Promise<TwoFactorProviderResponse>;
/**
* Enables or updates the email two-factor provider for the current user.
* Validates the email verification token sent via postTwoFactorEmailSetup before enabling.
* The token must match the code sent to the specified email address.
* Used for settings management.
*
* @param request The {@link UpdateTwoFactorEmailRequest} to prove authentication.
* @returns A promise that resolves to the updated email two-factor configuration.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise<TwoFactorEmailResponse>;
/**
* Enables or updates the Duo two-factor provider for the current user.
* Validates the Duo configuration (client ID, client secret, and host) before enabling.
* Requires user verification and an active premium subscription.
* Used for settings management.
*
* @param request The {@link UpdateTwoFactorDuoRequest} to prove authentication.
* @returns A promise that resolves to the updated Duo configuration.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise<TwoFactorDuoResponse>;
/**
* Enables or updates the Duo two-factor provider for an organization.
* Validates the Duo configuration (client ID, client secret, and host) before enabling.
* Requires user verification and organization policy management permissions.
* Used for settings management.
*
* @param organizationId The ID of the organization.
* @param request The {@link UpdateTwoFactorDuoRequest} to prove authentication.
* @returns A promise that resolves to the updated organization Duo configuration.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract putTwoFactorOrganizationDuo(
organizationId: string,
request: UpdateTwoFactorDuoRequest,
): Promise<TwoFactorDuoResponse>;
/**
* Enables or updates the YubiKey OTP two-factor provider for the current user.
* Validates each provided YubiKey by testing an OTP from the device.
* Supports up to 5 YubiKey devices. Empty key slots are allowed.
* Requires user verification and an active premium subscription.
* Used for settings management.
*
* @param request The {@link UpdateTwoFactorYubikeyOtpRequest} to prove authentication.
* @returns A promise that resolves to the updated YubiKey configuration.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract putTwoFactorYubiKey(
request: UpdateTwoFactorYubikeyOtpRequest,
): Promise<TwoFactorYubiKeyResponse>;
/**
* Registers a new WebAuthn (FIDO2) credential for two-factor authentication for the current user.
* Must be called after getTwoFactorWebAuthnChallenge to complete the registration flow.
* The device response contains the signed challenge from the authenticator device.
* Requires user verification via master password or OTP.
* Used for settings management.
*
* @param request The {@link UpdateTwoFactorWebAuthnRequest} to prove authentication.
* @returns A promise that resolves to the updated WebAuthn configuration with the new credential.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract putTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnRequest,
): Promise<TwoFactorWebAuthnResponse>;
/**
* Removes a specific WebAuthn (FIDO2) credential from the user's account.
* The credential will no longer be usable for two-factor authentication.
* Other registered WebAuthn credentials remain active.
* Requires user verification via master password or OTP.
* Used for settings management.
*
* @param request The {@link UpdateTwoFactorWebAuthnDeleteRequest} to prove authentication.
* @returns A promise that resolves to the updated WebAuthn configuration.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract deleteTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnDeleteRequest,
): Promise<TwoFactorWebAuthnResponse>;
/**
* Disables a specific two-factor provider for the current user.
* The provider will no longer be required or usable for authentication.
* Requires user verification via master password or OTP.
* Used for settings management.
*
* @param request The {@link TwoFactorProviderRequest} to prove authentication.
* @returns A promise that resolves to the updated provider status.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract putTwoFactorDisable(
request: TwoFactorProviderRequest,
): Promise<TwoFactorProviderResponse>;
/**
* Disables a specific two-factor provider for an organization.
* The provider will no longer be available for organization members.
* Requires user verification and organization policy management permissions.
* Used for settings management.
*
* @param organizationId The ID of the organization.
* @param request The {@link TwoFactorProviderRequest} to prove authentication.
* @returns A promise that resolves to the updated provider status.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract putTwoFactorOrganizationDisable(
organizationId: string,
request: TwoFactorProviderRequest,
): Promise<TwoFactorProviderResponse>;
/**
* Initiates email two-factor setup by sending a verification code to the specified email address.
* This is the first step in enabling email two-factor authentication.
* The verification code must be provided to putTwoFactorEmail to complete setup.
* Only used during initial configuration, not during login flows.
* Requires user verification via master password or OTP.
* Used for settings management.
*
* @param request The {@link TwoFactorEmailRequest} to prove authentication.
* @returns A promise that resolves when the verification email has been sent.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise<any>;
/**
* Sends a two-factor authentication code via email during the login flow.
* Supports multiple authentication contexts including standard login, SSO, and passwordless flows.
* This is used to deliver codes during authentication, not during initial setup.
* May be called without authentication for login scenarios.
* Used during authentication flows.
*
* @param request The {@link TwoFactorEmailRequest} to prove authentication.
* @returns A promise that resolves when the authentication email has been sent.
* @remarks Use {@link UserVerificationService.buildRequest} to create the request object.
*/
abstract postTwoFactorEmail(request: TwoFactorEmailRequest): Promise<any>;
}

View File

@@ -1,2 +1,2 @@
export { TwoFactorApiService } from "./two-factor-api.service";
export { DefaultTwoFactorApiService } from "./default-two-factor-api.service";
export * from "./abstractions";
export * from "./services";

View File

@@ -22,7 +22,7 @@ import { TwoFactorYubiKeyResponse } from "@bitwarden/common/auth/models/response
import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { TwoFactorApiService } from "./two-factor-api.service";
import { TwoFactorApiService } from "../abstractions/two-factor-api.service";
export class DefaultTwoFactorApiService implements TwoFactorApiService {
constructor(private apiService: ApiService) {}

View File

@@ -0,0 +1,279 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, map } from "rxjs";
import { TwoFactorApiService } from "..";
import { ListResponse } from "../../../models/response/list.response";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { Utils } from "../../../platform/misc/utils";
import { GlobalStateProvider } from "../../../platform/state";
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
import { DisableTwoFactorAuthenticatorRequest } from "../../models/request/disable-two-factor-authenticator.request";
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
import { TwoFactorEmailRequest } from "../../models/request/two-factor-email.request";
import { TwoFactorProviderRequest } from "../../models/request/two-factor-provider.request";
import { UpdateTwoFactorAuthenticatorRequest } from "../../models/request/update-two-factor-authenticator.request";
import { UpdateTwoFactorDuoRequest } from "../../models/request/update-two-factor-duo.request";
import { UpdateTwoFactorEmailRequest } from "../../models/request/update-two-factor-email.request";
import { UpdateTwoFactorWebAuthnDeleteRequest } from "../../models/request/update-two-factor-web-authn-delete.request";
import { UpdateTwoFactorWebAuthnRequest } from "../../models/request/update-two-factor-web-authn.request";
import { UpdateTwoFactorYubikeyOtpRequest } from "../../models/request/update-two-factor-yubikey-otp.request";
import { IdentityTwoFactorResponse } from "../../models/response/identity-two-factor.response";
import { TwoFactorAuthenticatorResponse } from "../../models/response/two-factor-authenticator.response";
import { TwoFactorDuoResponse } from "../../models/response/two-factor-duo.response";
import { TwoFactorEmailResponse } from "../../models/response/two-factor-email.response";
import { TwoFactorProviderResponse } from "../../models/response/two-factor-provider.response";
import { TwoFactorRecoverResponse } from "../../models/response/two-factor-recover.response";
import {
TwoFactorWebAuthnResponse,
ChallengeResponse,
} from "../../models/response/two-factor-web-authn.response";
import { TwoFactorYubiKeyResponse } from "../../models/response/two-factor-yubi-key.response";
import {
PROVIDERS,
SELECTED_PROVIDER,
TwoFactorProviderDetails,
TwoFactorProviders,
TwoFactorService as TwoFactorServiceAbstraction,
} from "../abstractions/two-factor.service";
export class DefaultTwoFactorService implements TwoFactorServiceAbstraction {
private providersState = this.globalStateProvider.get(PROVIDERS);
private selectedState = this.globalStateProvider.get(SELECTED_PROVIDER);
readonly providers$ = this.providersState.state$.pipe(
map((providers) => Utils.recordToMap(providers)),
);
readonly selected$ = this.selectedState.state$;
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private globalStateProvider: GlobalStateProvider,
private twoFactorApiService: TwoFactorApiService,
) {}
init() {
TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t("emailTitle");
TwoFactorProviders[TwoFactorProviderType.Email].description = this.i18nService.t("emailDescV2");
TwoFactorProviders[TwoFactorProviderType.Authenticator].name =
this.i18nService.t("authenticatorAppTitle");
TwoFactorProviders[TwoFactorProviderType.Authenticator].description =
this.i18nService.t("authenticatorAppDescV2");
TwoFactorProviders[TwoFactorProviderType.Duo].description = this.i18nService.t("duoDescV2");
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].name =
"Duo (" + this.i18nService.t("organization") + ")";
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].description =
this.i18nService.t("duoOrganizationDesc");
TwoFactorProviders[TwoFactorProviderType.WebAuthn].name = this.i18nService.t("webAuthnTitle");
TwoFactorProviders[TwoFactorProviderType.WebAuthn].description =
this.i18nService.t("webAuthnDesc");
TwoFactorProviders[TwoFactorProviderType.Yubikey].name = this.i18nService.t("yubiKeyTitleV2");
TwoFactorProviders[TwoFactorProviderType.Yubikey].description =
this.i18nService.t("yubiKeyDesc");
}
async getSupportedProviders(win: Window): Promise<TwoFactorProviderDetails[]> {
const data = await firstValueFrom(this.providers$);
const providers: any[] = [];
if (data == null) {
return providers;
}
if (
data.has(TwoFactorProviderType.OrganizationDuo) &&
this.platformUtilsService.supportsDuo()
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]);
}
if (data.has(TwoFactorProviderType.Authenticator)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]);
}
if (data.has(TwoFactorProviderType.Yubikey)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]);
}
if (data.has(TwoFactorProviderType.Duo) && this.platformUtilsService.supportsDuo()) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]);
}
if (
data.has(TwoFactorProviderType.WebAuthn) &&
this.platformUtilsService.supportsWebAuthn(win)
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]);
}
if (data.has(TwoFactorProviderType.Email)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Email]);
}
return providers;
}
async getDefaultProvider(webAuthnSupported: boolean): Promise<TwoFactorProviderType> {
const data = await firstValueFrom(this.providers$);
const selected = await firstValueFrom(this.selected$);
if (data == null) {
return null;
}
if (selected != null && data.has(selected)) {
return selected;
}
let providerType: TwoFactorProviderType = null;
let providerPriority = -1;
data.forEach((_value, type) => {
const provider = (TwoFactorProviders as any)[type];
if (provider != null && provider.priority > providerPriority) {
if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) {
return;
}
providerType = type;
providerPriority = provider.priority;
}
});
return providerType;
}
async setSelectedProvider(type: TwoFactorProviderType): Promise<void> {
await this.selectedState.update(() => type);
}
async clearSelectedProvider(): Promise<void> {
await this.selectedState.update(() => null);
}
async setProviders(response: IdentityTwoFactorResponse): Promise<void> {
await this.providersState.update(() => response.twoFactorProviders2);
}
async clearProviders(): Promise<void> {
await this.providersState.update(() => null);
}
getProviders(): Promise<Map<TwoFactorProviderType, { [key: string]: string }> | null> {
return firstValueFrom(this.providers$);
}
getEnabledTwoFactorProviders(): Promise<ListResponse<TwoFactorProviderResponse>> {
return this.twoFactorApiService.getTwoFactorProviders();
}
getTwoFactorOrganizationProviders(
organizationId: string,
): Promise<ListResponse<TwoFactorProviderResponse>> {
return this.twoFactorApiService.getTwoFactorOrganizationProviders(organizationId);
}
getTwoFactorAuthenticator(
request: SecretVerificationRequest,
): Promise<TwoFactorAuthenticatorResponse> {
return this.twoFactorApiService.getTwoFactorAuthenticator(request);
}
getTwoFactorEmail(request: SecretVerificationRequest): Promise<TwoFactorEmailResponse> {
return this.twoFactorApiService.getTwoFactorEmail(request);
}
getTwoFactorDuo(request: SecretVerificationRequest): Promise<TwoFactorDuoResponse> {
return this.twoFactorApiService.getTwoFactorDuo(request);
}
getTwoFactorOrganizationDuo(
organizationId: string,
request: SecretVerificationRequest,
): Promise<TwoFactorDuoResponse> {
return this.twoFactorApiService.getTwoFactorOrganizationDuo(organizationId, request);
}
getTwoFactorYubiKey(request: SecretVerificationRequest): Promise<TwoFactorYubiKeyResponse> {
return this.twoFactorApiService.getTwoFactorYubiKey(request);
}
getTwoFactorWebAuthn(request: SecretVerificationRequest): Promise<TwoFactorWebAuthnResponse> {
return this.twoFactorApiService.getTwoFactorWebAuthn(request);
}
getTwoFactorWebAuthnChallenge(request: SecretVerificationRequest): Promise<ChallengeResponse> {
return this.twoFactorApiService.getTwoFactorWebAuthnChallenge(request);
}
getTwoFactorRecover(request: SecretVerificationRequest): Promise<TwoFactorRecoverResponse> {
return this.twoFactorApiService.getTwoFactorRecover(request);
}
putTwoFactorAuthenticator(
request: UpdateTwoFactorAuthenticatorRequest,
): Promise<TwoFactorAuthenticatorResponse> {
return this.twoFactorApiService.putTwoFactorAuthenticator(request);
}
deleteTwoFactorAuthenticator(
request: DisableTwoFactorAuthenticatorRequest,
): Promise<TwoFactorProviderResponse> {
return this.twoFactorApiService.deleteTwoFactorAuthenticator(request);
}
putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise<TwoFactorEmailResponse> {
return this.twoFactorApiService.putTwoFactorEmail(request);
}
putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise<TwoFactorDuoResponse> {
return this.twoFactorApiService.putTwoFactorDuo(request);
}
putTwoFactorOrganizationDuo(
organizationId: string,
request: UpdateTwoFactorDuoRequest,
): Promise<TwoFactorDuoResponse> {
return this.twoFactorApiService.putTwoFactorOrganizationDuo(organizationId, request);
}
putTwoFactorYubiKey(
request: UpdateTwoFactorYubikeyOtpRequest,
): Promise<TwoFactorYubiKeyResponse> {
return this.twoFactorApiService.putTwoFactorYubiKey(request);
}
putTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnRequest,
): Promise<TwoFactorWebAuthnResponse> {
return this.twoFactorApiService.putTwoFactorWebAuthn(request);
}
deleteTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnDeleteRequest,
): Promise<TwoFactorWebAuthnResponse> {
return this.twoFactorApiService.deleteTwoFactorWebAuthn(request);
}
putTwoFactorDisable(request: TwoFactorProviderRequest): Promise<TwoFactorProviderResponse> {
return this.twoFactorApiService.putTwoFactorDisable(request);
}
putTwoFactorOrganizationDisable(
organizationId: string,
request: TwoFactorProviderRequest,
): Promise<TwoFactorProviderResponse> {
return this.twoFactorApiService.putTwoFactorOrganizationDisable(organizationId, request);
}
postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise<any> {
return this.twoFactorApiService.postTwoFactorEmailSetup(request);
}
postTwoFactorEmail(request: TwoFactorEmailRequest): Promise<any> {
return this.twoFactorApiService.postTwoFactorEmail(request);
}
}

View File

@@ -0,0 +1,2 @@
export * from "./default-two-factor-api.service";
export * from "./default-two-factor.service";

View File

@@ -16,7 +16,6 @@ export enum FeatureFlag {
BlockClaimedDomainAccountCreation = "block-claimed-domain-account-creation",
/* Auth */
PM22110_DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods",
PM23801_PrefetchPasswordPrelogin = "pm-23801-prefetch-password-prelogin",
/* Autofill */
@@ -118,7 +117,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.VaultLoadingSkeletons]: FALSE,
/* Auth */
[FeatureFlag.PM22110_DisableAlternateLoginMethods]: FALSE,
[FeatureFlag.PM23801_PrefetchPasswordPrelogin]: FALSE,
/* Billing */

1106
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -122,7 +122,7 @@
"json5": "2.2.3",
"lint-staged": "16.0.0",
"mini-css-extract-plugin": "2.9.2",
"nx": "21.3.11",
"nx": "21.6.8",
"postcss": "8.5.3",
"postcss-loader": "8.1.1",
"prettier": "3.6.2",
@@ -169,11 +169,11 @@
"@microsoft/signalr": "8.0.7",
"@microsoft/signalr-protocol-msgpack": "8.0.7",
"@ng-select/ng-select": "14.9.0",
"@nx/devkit": "21.3.11",
"@nx/eslint": "21.3.11",
"@nx/jest": "21.3.11",
"@nx/js": "21.3.11",
"@nx/webpack": "21.3.11",
"@nx/devkit": "21.6.8",
"@nx/eslint": "21.6.8",
"@nx/jest": "21.6.8",
"@nx/js": "21.6.8",
"@nx/webpack": "21.6.8",
"big-integer": "1.6.52",
"braintree-web-drop-in": "1.44.0",
"buffer": "6.0.3",
@@ -186,7 +186,7 @@
"inquirer": "8.2.6",
"jsdom": "26.1.0",
"jszip": "3.10.1",
"koa": "2.16.2",
"koa": "2.16.3",
"koa-bodyparser": "4.4.1",
"koa-json": "2.0.2",
"lit": "3.3.0",