1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 01:03:35 +00:00

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

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

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

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

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

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

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

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

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

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

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

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

View File

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