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

Revert "feat(two-factor-api-service) [PM-26465]: (Refactor) Two-Factor API Se…" (#16856)

This reverts commit 886003ba88.
This commit is contained in:
Dave
2025-10-13 14:22:49 -04:00
committed by GitHub
parent 886003ba88
commit d082d336e7
25 changed files with 383 additions and 1344 deletions

View File

@@ -127,7 +127,6 @@ import { UserVerificationService } from "@bitwarden/common/auth/services/user-ve
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 {
AutofillSettingsService,
AutofillSettingsServiceAbstraction,
@@ -1520,11 +1519,6 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultTwoFactorAuthWebAuthnComponentService,
deps: [],
}),
safeProvider({
provide: TwoFactorApiService,
useClass: DefaultTwoFactorApiService,
deps: [ApiServiceAbstraction],
}),
safeProvider({
provide: ViewCacheService,
useExisting: NoopViewCacheService,

View File

@@ -4,10 +4,10 @@ import { ReactiveFormsModule, FormsModule, FormControl } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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 { 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";
@@ -62,7 +62,7 @@ export class TwoFactorAuthEmailComponent implements OnInit {
protected loginStrategyService: LoginStrategyServiceAbstraction,
protected platformUtilsService: PlatformUtilsService,
protected logService: LogService,
protected twoFactorApiService: TwoFactorApiService,
protected apiService: ApiService,
protected appIdService: AppIdService,
private toastService: ToastService,
private cacheService: TwoFactorAuthEmailComponentCacheService,
@@ -131,7 +131,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.apiService.postTwoFactorEmail(request);
await this.emailPromise;
this.emailSent = true;

View File

@@ -37,6 +37,8 @@ import {
ProviderUserUserDetailsResponse,
} from "../admin-console/models/response/provider/provider-user.response";
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
import { EmailRequest } from "../auth/models/request/email.request";
import { PasswordTokenRequest } from "../auth/models/request/identity-token/password-token.request";
@@ -46,15 +48,34 @@ import { WebAuthnLoginTokenRequest } from "../auth/models/request/identity-token
import { PasswordHintRequest } from "../auth/models/request/password-hint.request";
import { PasswordlessAuthRequest } from "../auth/models/request/passwordless-auth.request";
import { SecretVerificationRequest } from "../auth/models/request/secret-verification.request";
import { TwoFactorEmailRequest } from "../auth/models/request/two-factor-email.request";
import { TwoFactorProviderRequest } from "../auth/models/request/two-factor-provider.request";
import { UpdateProfileRequest } from "../auth/models/request/update-profile.request";
import { UpdateTwoFactorAuthenticatorRequest } from "../auth/models/request/update-two-factor-authenticator.request";
import { UpdateTwoFactorDuoRequest } from "../auth/models/request/update-two-factor-duo.request";
import { UpdateTwoFactorEmailRequest } from "../auth/models/request/update-two-factor-email.request";
import { UpdateTwoFactorWebAuthnDeleteRequest } from "../auth/models/request/update-two-factor-web-authn-delete.request";
import { UpdateTwoFactorWebAuthnRequest } from "../auth/models/request/update-two-factor-web-authn.request";
import { UpdateTwoFactorYubikeyOtpRequest } from "../auth/models/request/update-two-factor-yubikey-otp.request";
import { ApiKeyResponse } from "../auth/models/response/api-key.response";
import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response";
import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response";
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
import { PreloginResponse } from "../auth/models/response/prelogin.response";
import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response";
import { TwoFactorAuthenticatorResponse } from "../auth/models/response/two-factor-authenticator.response";
import { TwoFactorDuoResponse } from "../auth/models/response/two-factor-duo.response";
import { TwoFactorEmailResponse } from "../auth/models/response/two-factor-email.response";
import { TwoFactorProviderResponse } from "../auth/models/response/two-factor-provider.response";
import { TwoFactorRecoverResponse } from "../auth/models/response/two-factor-recover.response";
import {
ChallengeResponse,
TwoFactorWebAuthnResponse,
} from "../auth/models/response/two-factor-web-authn.response";
import { TwoFactorYubiKeyResponse } from "../auth/models/response/two-factor-yubi-key.response";
import { BitPayInvoiceRequest } from "../billing/models/request/bit-pay-invoice.request";
import { BillingHistoryResponse } from "../billing/models/response/billing-history.response";
import { PaymentResponse } from "../billing/models/response/payment.response";
@@ -285,6 +306,66 @@ export abstract class ApiService {
abstract getSettingsDomains(): Promise<DomainsResponse>;
abstract putSettingsDomains(request: UpdateDomainsRequest): Promise<DomainsResponse>;
abstract getTwoFactorProviders(): Promise<ListResponse<TwoFactorProviderResponse>>;
abstract getTwoFactorOrganizationProviders(
organizationId: string,
): Promise<ListResponse<TwoFactorProviderResponse>>;
abstract getTwoFactorAuthenticator(
request: SecretVerificationRequest,
): Promise<TwoFactorAuthenticatorResponse>;
abstract getTwoFactorEmail(request: SecretVerificationRequest): Promise<TwoFactorEmailResponse>;
abstract getTwoFactorDuo(request: SecretVerificationRequest): Promise<TwoFactorDuoResponse>;
abstract getTwoFactorOrganizationDuo(
organizationId: string,
request: SecretVerificationRequest,
): Promise<TwoFactorDuoResponse>;
abstract getTwoFactorYubiKey(
request: SecretVerificationRequest,
): Promise<TwoFactorYubiKeyResponse>;
abstract getTwoFactorWebAuthn(
request: SecretVerificationRequest,
): Promise<TwoFactorWebAuthnResponse>;
abstract getTwoFactorWebAuthnChallenge(
request: SecretVerificationRequest,
): Promise<ChallengeResponse>;
abstract getTwoFactorRecover(
request: SecretVerificationRequest,
): Promise<TwoFactorRecoverResponse>;
abstract putTwoFactorAuthenticator(
request: UpdateTwoFactorAuthenticatorRequest,
): Promise<TwoFactorAuthenticatorResponse>;
abstract deleteTwoFactorAuthenticator(
request: DisableTwoFactorAuthenticatorRequest,
): Promise<TwoFactorProviderResponse>;
abstract putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise<TwoFactorEmailResponse>;
abstract putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise<TwoFactorDuoResponse>;
abstract putTwoFactorOrganizationDuo(
organizationId: string,
request: UpdateTwoFactorDuoRequest,
): Promise<TwoFactorDuoResponse>;
abstract putTwoFactorYubiKey(
request: UpdateTwoFactorYubikeyOtpRequest,
): Promise<TwoFactorYubiKeyResponse>;
abstract putTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnRequest,
): Promise<TwoFactorWebAuthnResponse>;
abstract deleteTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnDeleteRequest,
): Promise<TwoFactorWebAuthnResponse>;
abstract putTwoFactorDisable(
request: TwoFactorProviderRequest,
): Promise<TwoFactorProviderResponse>;
abstract putTwoFactorOrganizationDisable(
organizationId: string,
request: TwoFactorProviderRequest,
): Promise<TwoFactorProviderResponse>;
abstract postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise<any>;
abstract postTwoFactorEmail(request: TwoFactorEmailRequest): Promise<any>;
abstract getDeviceVerificationSettings(): Promise<DeviceVerificationResponse>;
abstract putDeviceVerificationSettings(
request: DeviceVerificationRequest,
): Promise<DeviceVerificationResponse>;
abstract getCloudCommunicationsEnabled(): Promise<boolean>;
abstract getOrganizationConnection<TConfig extends OrganizationConnectionConfigApis>(
id: string,

View File

@@ -0,0 +1,7 @@
export class DeviceVerificationRequest {
unknownDeviceVerificationEnabled: boolean;
constructor(unknownDeviceVerificationEnabled: boolean) {
this.unknownDeviceVerificationEnabled = unknownDeviceVerificationEnabled;
}
}

View File

@@ -0,0 +1,16 @@
import { BaseResponse } from "../../../models/response/base.response";
export class DeviceVerificationResponse extends BaseResponse {
isDeviceVerificationSectionEnabled: boolean;
unknownDeviceVerificationEnabled: boolean;
constructor(response: any) {
super(response);
this.isDeviceVerificationSectionEnabled = this.getResponseProperty(
"IsDeviceVerificationSectionEnabled",
);
this.unknownDeviceVerificationEnabled = this.getResponseProperty(
"UnknownDeviceVerificationEnabled",
);
}
}

View File

@@ -1,272 +0,0 @@
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { DisableTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/disable-two-factor-authenticator.request";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { TwoFactorProviderRequest } from "@bitwarden/common/auth/models/request/two-factor-provider.request";
import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/update-two-factor-authenticator.request";
import { UpdateTwoFactorDuoRequest } from "@bitwarden/common/auth/models/request/update-two-factor-duo.request";
import { UpdateTwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/update-two-factor-email.request";
import { UpdateTwoFactorWebAuthnDeleteRequest } from "@bitwarden/common/auth/models/request/update-two-factor-web-authn-delete.request";
import { UpdateTwoFactorWebAuthnRequest } from "@bitwarden/common/auth/models/request/update-two-factor-web-authn.request";
import { UpdateTwoFactorYubikeyOtpRequest } from "@bitwarden/common/auth/models/request/update-two-factor-yubikey-otp.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 { TwoFactorProviderResponse } from "@bitwarden/common/auth/models/response/two-factor-provider.response";
import { TwoFactorRecoverResponse } from "@bitwarden/common/auth/models/response/two-factor-recover.response";
import {
TwoFactorWebAuthnResponse,
ChallengeResponse,
} 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 { ListResponse } from "@bitwarden/common/models/response/list.response";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { TwoFactorApiService } from "./two-factor-api.service";
export class DefaultTwoFactorApiService implements TwoFactorApiService {
constructor(private apiService: ApiService) {}
// Providers
async getTwoFactorProviders(): Promise<ListResponse<TwoFactorProviderResponse>> {
const response = await this.apiService.send("GET", "/two-factor", null, true, true);
return new ListResponse(response, TwoFactorProviderResponse);
}
async getTwoFactorOrganizationProviders(
organizationId: string,
): Promise<ListResponse<TwoFactorProviderResponse>> {
const response = await this.apiService.send(
"GET",
`/organizations/${organizationId}/two-factor`,
null,
true,
true,
);
return new ListResponse(response, TwoFactorProviderResponse);
}
// Authenticator (TOTP)
async getTwoFactorAuthenticator(
request: SecretVerificationRequest,
): Promise<TwoFactorAuthenticatorResponse> {
const response = await this.apiService.send(
"POST",
"/two-factor/get-authenticator",
request,
true,
true,
);
return new TwoFactorAuthenticatorResponse(response);
}
async putTwoFactorAuthenticator(
request: UpdateTwoFactorAuthenticatorRequest,
): Promise<TwoFactorAuthenticatorResponse> {
const response = await this.apiService.send(
"PUT",
"/two-factor/authenticator",
request,
true,
true,
);
return new TwoFactorAuthenticatorResponse(response);
}
async deleteTwoFactorAuthenticator(
request: DisableTwoFactorAuthenticatorRequest,
): Promise<TwoFactorProviderResponse> {
const response = await this.apiService.send(
"DELETE",
"/two-factor/authenticator",
request,
true,
true,
);
return new TwoFactorProviderResponse(response);
}
// Email
async getTwoFactorEmail(request: SecretVerificationRequest): Promise<TwoFactorEmailResponse> {
const response = await this.apiService.send(
"POST",
"/two-factor/get-email",
request,
true,
true,
);
return new TwoFactorEmailResponse(response);
}
async postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise<any> {
return this.apiService.send("POST", "/two-factor/send-email", request, true, false);
}
async postTwoFactorEmail(request: TwoFactorEmailRequest): Promise<any> {
return this.apiService.send("POST", "/two-factor/send-email-login", request, false, false);
}
async putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise<TwoFactorEmailResponse> {
const response = await this.apiService.send("PUT", "/two-factor/email", request, true, true);
return new TwoFactorEmailResponse(response);
}
// Duo
async getTwoFactorDuo(request: SecretVerificationRequest): Promise<TwoFactorDuoResponse> {
const response = await this.apiService.send("POST", "/two-factor/get-duo", request, true, true);
return new TwoFactorDuoResponse(response);
}
async getTwoFactorOrganizationDuo(
organizationId: string,
request: SecretVerificationRequest,
): Promise<TwoFactorDuoResponse> {
const response = await this.apiService.send(
"POST",
`/organizations/${organizationId}/two-factor/get-duo`,
request,
true,
true,
);
return new TwoFactorDuoResponse(response);
}
async putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise<TwoFactorDuoResponse> {
const response = await this.apiService.send("PUT", "/two-factor/duo", request, true, true);
return new TwoFactorDuoResponse(response);
}
async putTwoFactorOrganizationDuo(
organizationId: string,
request: UpdateTwoFactorDuoRequest,
): Promise<TwoFactorDuoResponse> {
const response = await this.apiService.send(
"PUT",
`/organizations/${organizationId}/two-factor/duo`,
request,
true,
true,
);
return new TwoFactorDuoResponse(response);
}
// YubiKey
async getTwoFactorYubiKey(request: SecretVerificationRequest): Promise<TwoFactorYubiKeyResponse> {
const response = await this.apiService.send(
"POST",
"/two-factor/get-yubikey",
request,
true,
true,
);
return new TwoFactorYubiKeyResponse(response);
}
async putTwoFactorYubiKey(
request: UpdateTwoFactorYubikeyOtpRequest,
): Promise<TwoFactorYubiKeyResponse> {
const response = await this.apiService.send("PUT", "/two-factor/yubikey", request, true, true);
return new TwoFactorYubiKeyResponse(response);
}
// WebAuthn
async getTwoFactorWebAuthn(
request: SecretVerificationRequest,
): Promise<TwoFactorWebAuthnResponse> {
const response = await this.apiService.send(
"POST",
"/two-factor/get-webauthn",
request,
true,
true,
);
return new TwoFactorWebAuthnResponse(response);
}
async getTwoFactorWebAuthnChallenge(
request: SecretVerificationRequest,
): Promise<ChallengeResponse> {
const response = await this.apiService.send(
"POST",
"/two-factor/get-webauthn-challenge",
request,
true,
true,
);
return new ChallengeResponse(response);
}
async putTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnRequest,
): Promise<TwoFactorWebAuthnResponse> {
const deviceResponse = request.deviceResponse.response as AuthenticatorAttestationResponse;
const body: any = Object.assign({}, request);
body.deviceResponse = {
id: request.deviceResponse.id,
rawId: btoa(request.deviceResponse.id),
type: request.deviceResponse.type,
extensions: request.deviceResponse.getClientExtensionResults(),
response: {
AttestationObject: Utils.fromBufferToB64(deviceResponse.attestationObject),
clientDataJson: Utils.fromBufferToB64(deviceResponse.clientDataJSON),
},
};
const response = await this.apiService.send("PUT", "/two-factor/webauthn", body, true, true);
return new TwoFactorWebAuthnResponse(response);
}
async deleteTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnDeleteRequest,
): Promise<TwoFactorWebAuthnResponse> {
const response = await this.apiService.send(
"DELETE",
"/two-factor/webauthn",
request,
true,
true,
);
return new TwoFactorWebAuthnResponse(response);
}
// Recovery Code
async getTwoFactorRecover(request: SecretVerificationRequest): Promise<TwoFactorRecoverResponse> {
const response = await this.apiService.send(
"POST",
"/two-factor/get-recover",
request,
true,
true,
);
return new TwoFactorRecoverResponse(response);
}
// Disable
async putTwoFactorDisable(request: TwoFactorProviderRequest): Promise<TwoFactorProviderResponse> {
const response = await this.apiService.send("PUT", "/two-factor/disable", request, true, true);
return new TwoFactorProviderResponse(response);
}
async putTwoFactorOrganizationDisable(
organizationId: string,
request: TwoFactorProviderRequest,
): Promise<TwoFactorProviderResponse> {
const response = await this.apiService.send(
"PUT",
`/organizations/${organizationId}/two-factor/disable`,
request,
true,
true,
);
return new TwoFactorProviderResponse(response);
}
}

View File

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

View File

@@ -1,697 +0,0 @@
import { mock, MockProxy } from "jest-mock-extended";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { DisableTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/disable-two-factor-authenticator.request";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { TwoFactorProviderRequest } from "@bitwarden/common/auth/models/request/two-factor-provider.request";
import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/update-two-factor-authenticator.request";
import { UpdateTwoFactorDuoRequest } from "@bitwarden/common/auth/models/request/update-two-factor-duo.request";
import { UpdateTwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/update-two-factor-email.request";
import { UpdateTwoFactorWebAuthnDeleteRequest } from "@bitwarden/common/auth/models/request/update-two-factor-web-authn-delete.request";
import { UpdateTwoFactorWebAuthnRequest } from "@bitwarden/common/auth/models/request/update-two-factor-web-authn.request";
import { UpdateTwoFactorYubikeyOtpRequest } from "@bitwarden/common/auth/models/request/update-two-factor-yubikey-otp.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 { TwoFactorProviderResponse } from "@bitwarden/common/auth/models/response/two-factor-provider.response";
import { TwoFactorRecoverResponse } from "@bitwarden/common/auth/models/response/two-factor-recover.response";
import {
TwoFactorWebAuthnResponse,
ChallengeResponse,
} 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 { ListResponse } from "@bitwarden/common/models/response/list.response";
import { DefaultTwoFactorApiService } from "./default-two-factor-api.service";
describe("TwoFactorApiService", () => {
let apiService: MockProxy<ApiService>;
let twoFactorApiService: DefaultTwoFactorApiService;
beforeEach(() => {
apiService = mock<ApiService>();
twoFactorApiService = new DefaultTwoFactorApiService(apiService);
});
describe("Two-Factor Providers", () => {
describe("getTwoFactorProviders", () => {
it("retrieves all enabled two-factor providers for the current user", async () => {
const mockResponse = {
data: [
{ Type: 0, Enabled: true },
{ Type: 1, Enabled: true },
],
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.getTwoFactorProviders();
expect(apiService.send).toHaveBeenCalledWith("GET", "/two-factor", null, true, true);
expect(result).toBeInstanceOf(ListResponse);
expect(result.data).toHaveLength(2);
for (let i = 0; i < result.data.length; i++) {
expect(result.data[i]).toBeInstanceOf(TwoFactorProviderResponse);
expect(result.data[i].type).toBe(i);
expect(result.data[i].enabled).toBe(true);
}
});
});
describe("getTwoFactorOrganizationProviders", () => {
it("retrieves all enabled two-factor providers for a specific organization", async () => {
const organizationId = "org-123";
const mockResponse = {
data: [{ Type: 6, Enabled: true }],
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.getTwoFactorOrganizationProviders(organizationId);
expect(apiService.send).toHaveBeenCalledWith(
"GET",
`/organizations/${organizationId}/two-factor`,
null,
true,
true,
);
expect(result).toBeInstanceOf(ListResponse);
expect(result.data[0]).toBeInstanceOf(TwoFactorProviderResponse);
expect(result.data[0].enabled).toBe(true);
expect(result.data[0].type).toBe(6); // Duo
});
});
});
describe("Authenticator (TOTP) APIs", () => {
describe("getTwoFactorAuthenticator", () => {
it("retrieves authenticator configuration with secret key after user verification", async () => {
const request = new SecretVerificationRequest();
request.masterPasswordHash = "master-password-hash";
const mockResponse = {
Enabled: false,
Key: "MFRGGZDFMZTWQ2LK",
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.getTwoFactorAuthenticator(request);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
"/two-factor/get-authenticator",
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorAuthenticatorResponse);
expect(result.enabled).toBe(false);
});
});
describe("putTwoFactorAuthenticator", () => {
it("enables authenticator after validating the provided token", async () => {
const request = new UpdateTwoFactorAuthenticatorRequest();
request.token = "123456";
request.key = "MFRGGZDFMZTWQ2LK";
const mockResponse = {
Enabled: true,
Key: "MFRGGZDFMZTWQ2LK",
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.putTwoFactorAuthenticator(request);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
"/two-factor/authenticator",
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorAuthenticatorResponse);
expect(result.enabled).toBe(true);
expect(result.key).toBeDefined();
});
});
describe("deleteTwoFactorAuthenticator", () => {
it("disables authenticator two-factor authentication", async () => {
const request = new DisableTwoFactorAuthenticatorRequest();
request.masterPasswordHash = "master-password-hash";
const mockResponse = {
Enabled: false,
Type: 0,
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.deleteTwoFactorAuthenticator(request);
expect(apiService.send).toHaveBeenCalledWith(
"DELETE",
"/two-factor/authenticator",
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorProviderResponse);
expect(result.enabled).toBe(false);
expect(result.type).toBe(0); // Authenticator
});
});
});
describe("Email APIs", () => {
describe("getTwoFactorEmail", () => {
it("retrieves email two-factor configuration after user verification", async () => {
const request = new SecretVerificationRequest();
request.masterPasswordHash = "master-password-hash";
const mockResponse = {
Enabled: true,
Email: "user@example.com",
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.getTwoFactorEmail(request);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
"/two-factor/get-email",
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorEmailResponse);
expect(result.enabled).toBe(true);
expect(result.email).toBeDefined();
});
});
describe("postTwoFactorEmailSetup", () => {
it("sends verification code to email address during two-factor setup", async () => {
const request = new TwoFactorEmailRequest();
request.email = "user@example.com";
request.masterPasswordHash = "master-password-hash";
await twoFactorApiService.postTwoFactorEmailSetup(request);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
"/two-factor/send-email",
request,
true,
false,
);
});
});
describe("postTwoFactorEmail", () => {
it("sends two-factor authentication code during login flow", async () => {
const request = new TwoFactorEmailRequest();
request.email = "user@example.com";
// Note: masterPasswordHash not required for login flow
await twoFactorApiService.postTwoFactorEmail(request);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
"/two-factor/send-email-login",
request,
false,
false,
);
});
});
describe("putTwoFactorEmail", () => {
it("enables email two-factor after validating the verification code", async () => {
const request = new UpdateTwoFactorEmailRequest();
request.email = "user@example.com";
request.token = "verification-code";
const mockResponse = {
Enabled: true,
Email: "user@example.com",
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.putTwoFactorEmail(request);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
"/two-factor/email",
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorEmailResponse);
expect(result.enabled).toBe(true);
expect(result.email).toBeDefined();
});
});
});
describe("Duo APIs", () => {
describe("getTwoFactorDuo", () => {
it("retrieves Duo configuration for premium user after verification", async () => {
const request = new SecretVerificationRequest();
request.masterPasswordHash = "master-password-hash";
const mockResponse = {
Enabled: true,
Host: "api-abc123.duosecurity.com",
ClientId: "DI9ABC1DEFGH2JKL",
ClientSecret: "client******",
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.getTwoFactorDuo(request);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
"/two-factor/get-duo",
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorDuoResponse);
expect(result.enabled).toBe(true);
expect(result.host).toBeDefined();
expect(result.clientId).toBeDefined();
expect(result.clientSecret).toContain("******");
});
});
describe("getTwoFactorOrganizationDuo", () => {
it("retrieves Duo configuration for organization with admin permissions", async () => {
const organizationId = "org-123";
const request = new SecretVerificationRequest();
request.masterPasswordHash = "master-password-hash";
const mockResponse = {
Enabled: true,
Host: "api-xyz789.duosecurity.com",
ClientId: "DI4XYZ9MNOP3QRS",
ClientSecret: "orgcli******",
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.getTwoFactorOrganizationDuo(
organizationId,
request,
);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
`/organizations/${organizationId}/two-factor/get-duo`,
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorDuoResponse);
expect(result.enabled).toBe(true);
expect(result.host).toBeDefined();
expect(result.clientId).toBeDefined();
expect(result.clientSecret).toContain("******");
});
});
describe("putTwoFactorDuo", () => {
it("enables Duo two-factor for premium user with valid integration details", async () => {
const request = new UpdateTwoFactorDuoRequest();
request.host = "api-abc123.duosecurity.com";
request.clientId = "DI9ABC1DEFGH2JKL";
request.clientSecret = "client-secret-value-here";
const mockResponse = {
Enabled: true,
Host: "api-abc123.duosecurity.com",
ClientId: "DI9ABC1DEFGH2JKL",
ClientSecret: "client******",
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.putTwoFactorDuo(request);
expect(apiService.send).toHaveBeenCalledWith("PUT", "/two-factor/duo", request, true, true);
expect(result).toBeInstanceOf(TwoFactorDuoResponse);
expect(result.enabled).toBe(true);
expect(result.host).toBeDefined();
expect(result.clientId).toBeDefined();
expect(result.clientSecret).toContain("******");
});
});
describe("putTwoFactorOrganizationDuo", () => {
it("enables organization-level Duo with policy management permissions", async () => {
const organizationId = "org-123";
const request = new UpdateTwoFactorDuoRequest();
request.host = "api-xyz789.duosecurity.com";
request.clientId = "DI4XYZ9MNOP3QRS";
request.clientSecret = "orgcli-secret-value-here";
const mockResponse = {
Enabled: true,
Host: "api-xyz789.duosecurity.com",
ClientId: "DI4XYZ9MNOP3QRS",
ClientSecret: "orgcli******",
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.putTwoFactorOrganizationDuo(
organizationId,
request,
);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
`/organizations/${organizationId}/two-factor/duo`,
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorDuoResponse);
expect(result.enabled).toBe(true);
expect(result.host).toBeDefined();
expect(result.clientId).toBeDefined();
expect(result.clientSecret).toContain("******");
});
});
});
describe("YubiKey APIs", () => {
describe("getTwoFactorYubiKey", () => {
it("retrieves YubiKey configuration for premium user after verification", async () => {
const request = new SecretVerificationRequest();
request.masterPasswordHash = "master-password-hash";
const mockResponse = {
Enabled: true,
Key1: "cccccccccccc",
Key2: "dddddddddddd",
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.getTwoFactorYubiKey(request);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
"/two-factor/get-yubikey",
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorYubiKeyResponse);
expect(result.enabled).toBe(true);
expect(result.key1).toBeDefined();
expect(result.key2).toBeDefined();
});
});
describe("putTwoFactorYubiKey", () => {
it("enables YubiKey two-factor for premium user after validating device OTPs", async () => {
const request = new UpdateTwoFactorYubikeyOtpRequest();
request.key1 = "ccccccccccccjkhbhbhrkcitringjkrjirfjuunlnlvcghnkrtgfj";
request.key2 = "ddddddddddddvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv";
const mockResponse = {
Enabled: true,
Key1: "cccccccccccc",
Key2: "dddddddddddd",
Nfc: false,
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.putTwoFactorYubiKey(request);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
"/two-factor/yubikey",
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorYubiKeyResponse);
expect(result.enabled).toBe(true);
expect(result.key1).toBeDefined();
expect(result.key2).toBeDefined();
});
});
});
describe("WebAuthn APIs", () => {
describe("getTwoFactorWebAuthn", () => {
it("retrieves list of registered WebAuthn credentials after verification", async () => {
const request = new SecretVerificationRequest();
request.masterPasswordHash = "master-password-hash";
const mockResponse = {
Enabled: true,
Keys: [
{ Name: "YubiKey 5", Id: 1, Migrated: false },
{ Name: "Security Key", Id: 2, Migrated: true },
],
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.getTwoFactorWebAuthn(request);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
"/two-factor/get-webauthn",
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorWebAuthnResponse);
expect(result.enabled).toBe(true);
expect(result.keys).toHaveLength(2);
result.keys.forEach((key) => {
expect(key).toHaveProperty("name");
expect(key).toHaveProperty("id");
expect(key).toHaveProperty("migrated");
});
});
});
describe("getTwoFactorWebAuthnChallenge", () => {
it("obtains cryptographic challenge for WebAuthn credential registration", async () => {
const request = new SecretVerificationRequest();
request.masterPasswordHash = "master-password-hash";
const mockResponse = {
challenge: "Y2hhbGxlbmdlLXN0cmluZw",
rp: { name: "Bitwarden" },
user: {
id: "dXNlci1pZA",
name: "user@example.com",
displayName: "User",
},
pubKeyCredParams: [{ type: "public-key", alg: -7 }], // ES256
excludeCredentials: [] as PublicKeyCredentialDescriptor[],
timeout: 60000,
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.getTwoFactorWebAuthnChallenge(request);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
"/two-factor/get-webauthn-challenge",
request,
true,
true,
);
expect(result).toBeInstanceOf(ChallengeResponse);
expect(result.challenge).toBeDefined();
expect(result.rp).toHaveProperty("name", "Bitwarden");
expect(result.user).toHaveProperty("id");
expect(result.user).toHaveProperty("name");
expect(result.user).toHaveProperty("displayName", "User");
expect(result.pubKeyCredParams).toHaveLength(1);
expect(Number(result.timeout)).toBeTruthy();
});
});
describe("putTwoFactorWebAuthn", () => {
it("registers new WebAuthn credential by serializing browser credential to JSON", async () => {
const mockAttestationResponse: Partial<AuthenticatorAttestationResponse> = {
clientDataJSON: new Uint8Array([1, 2, 3]).buffer,
attestationObject: new Uint8Array([4, 5, 6]).buffer,
};
const mockCredential: Partial<PublicKeyCredential> = {
id: "credential-id",
type: "public-key",
response: mockAttestationResponse as AuthenticatorAttestationResponse,
getClientExtensionResults: jest.fn().mockReturnValue({}),
};
const request = new UpdateTwoFactorWebAuthnRequest();
request.deviceResponse = mockCredential as PublicKeyCredential;
request.name = "My Security Key";
const mockResponse = {
Enabled: true,
Keys: [{ Name: "My Security Key", Id: 1, Migrated: false }],
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.putTwoFactorWebAuthn(request);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
"/two-factor/webauthn",
expect.objectContaining({
name: "My Security Key",
deviceResponse: expect.objectContaining({
id: "credential-id",
rawId: expect.any(String), // base64 encoded
type: "public-key",
extensions: {},
response: expect.objectContaining({
AttestationObject: expect.any(String), // base64 encoded
clientDataJson: expect.any(String), // base64 encoded
}),
}),
}),
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorWebAuthnResponse);
expect(result.enabled).toBe(true);
expect(result.keys).toHaveLength(1);
expect(result.keys[0].name).toBeDefined();
expect(result.keys[0].id).toBeDefined();
expect(result.keys[0].migrated).toBeDefined();
});
it("preserves original request object without mutation during serialization", async () => {
const mockAttestationResponse: Partial<AuthenticatorAttestationResponse> = {
clientDataJSON: new Uint8Array([1, 2, 3]).buffer,
attestationObject: new Uint8Array([4, 5, 6]).buffer,
};
const mockCredential: Partial<PublicKeyCredential> = {
id: "credential-id",
type: "public-key",
response: mockAttestationResponse as AuthenticatorAttestationResponse,
getClientExtensionResults: jest.fn().mockReturnValue({}),
};
const request = new UpdateTwoFactorWebAuthnRequest();
request.deviceResponse = mockCredential as PublicKeyCredential;
request.name = "My Security Key";
const originalDeviceResponse = request.deviceResponse;
apiService.send.mockResolvedValue({ enabled: true, keys: [] });
await twoFactorApiService.putTwoFactorWebAuthn(request);
// Do not mutate the original request object
expect(request.deviceResponse).toBe(originalDeviceResponse);
expect(request.deviceResponse.response).toBe(mockAttestationResponse);
});
});
describe("deleteTwoFactorWebAuthn", () => {
it("removes specific WebAuthn credential while preserving other registered keys", async () => {
const request = new UpdateTwoFactorWebAuthnDeleteRequest();
request.id = 1;
request.masterPasswordHash = "master-password-hash";
const mockResponse = {
Enabled: true,
Keys: [{ Name: "Security Key", Id: 2, Migrated: true }], // Key with id:1 removed
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.deleteTwoFactorWebAuthn(request);
expect(apiService.send).toHaveBeenCalledWith(
"DELETE",
"/two-factor/webauthn",
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorWebAuthnResponse);
expect(result.keys).toHaveLength(1);
expect(result.keys[0].id).toBe(2);
});
});
});
describe("Recovery Code APIs", () => {
describe("getTwoFactorRecover", () => {
it("retrieves recovery code for regaining access when two-factor is unavailable", async () => {
const request = new SecretVerificationRequest();
request.masterPasswordHash = "master-password-hash";
const mockResponse = {
Code: "ABCD-EFGH-IJKL-MNOP-QRST-UVWX-YZ12",
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.getTwoFactorRecover(request);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
"/two-factor/get-recover",
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorRecoverResponse);
expect(result.code).toBeDefined();
expect(result.code).toMatch(/^[A-Z0-9-]+$/);
});
});
});
describe("Disable APIs", () => {
describe("putTwoFactorDisable", () => {
it("disables specified two-factor provider for current user", async () => {
const request = new TwoFactorProviderRequest();
request.type = 0; // Authenticator
request.masterPasswordHash = "master-password-hash";
const mockResponse = {
Enabled: false,
Type: 0,
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.putTwoFactorDisable(request);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
"/two-factor/disable",
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorProviderResponse);
expect(result.enabled).toBe(false);
expect(result.type).toBe(0); // Authenticator
});
});
describe("putTwoFactorOrganizationDisable", () => {
it("disables two-factor provider for organization with policy management permissions", async () => {
const organizationId = "org-123";
const request = new TwoFactorProviderRequest();
request.type = 6; // Duo
request.masterPasswordHash = "master-password-hash";
const mockResponse = {
Enabled: false,
Type: 6,
};
apiService.send.mockResolvedValue(mockResponse);
const result = await twoFactorApiService.putTwoFactorOrganizationDisable(
organizationId,
request,
);
expect(apiService.send).toHaveBeenCalledWith(
"PUT",
`/organizations/${organizationId}/two-factor/disable`,
request,
true,
true,
);
expect(result).toBeInstanceOf(TwoFactorProviderResponse);
expect(result.enabled).toBe(false);
expect(result.type).toBe(6); // Duo
});
});
});
});

View File

@@ -1,292 +0,0 @@
import { DisableTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/disable-two-factor-authenticator.request";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { TwoFactorProviderRequest } from "@bitwarden/common/auth/models/request/two-factor-provider.request";
import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/update-two-factor-authenticator.request";
import { UpdateTwoFactorDuoRequest } from "@bitwarden/common/auth/models/request/update-two-factor-duo.request";
import { UpdateTwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/update-two-factor-email.request";
import { UpdateTwoFactorWebAuthnDeleteRequest } from "@bitwarden/common/auth/models/request/update-two-factor-web-authn-delete.request";
import { UpdateTwoFactorWebAuthnRequest } from "@bitwarden/common/auth/models/request/update-two-factor-web-authn.request";
import { UpdateTwoFactorYubikeyOtpRequest } from "@bitwarden/common/auth/models/request/update-two-factor-yubikey-otp.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 { TwoFactorProviderResponse } from "@bitwarden/common/auth/models/response/two-factor-provider.response";
import { TwoFactorRecoverResponse } from "@bitwarden/common/auth/models/response/two-factor-recover.response";
import {
ChallengeResponse,
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 { ListResponse } from "@bitwarden/common/models/response/list.response";
/**
* Service abstraction for two-factor authentication API operations.
* Provides methods for managing various two-factor authentication providers including
* authenticator apps (TOTP), email, Duo, YubiKey, WebAuthn (FIDO2), and recovery codes.
*
* All methods that retrieve sensitive configuration data require user verification via
* SecretVerificationRequest. Premium-tier providers (Duo, YubiKey) require an active
* premium subscription. Organization-level methods require appropriate administrative permissions.
*/
export abstract class TwoFactorApiService {
/**
* Gets a list of all enabled two-factor providers for the current user.
*
* @returns A promise that resolves to a list response containing enabled two-factor provider configurations.
*/
abstract getTwoFactorProviders(): Promise<ListResponse<TwoFactorProviderResponse>>;
/**
* Gets a list of all enabled two-factor providers for an organization.
* Requires organization administrator permissions.
*
* @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.
* Returns the shared secret key and user verification token needed for setup.
* Requires user verification via master password or OTP.
*
* @param request The secret verification request to authorize the operation.
* @returns A promise that resolves to the authenticator configuration including the secret key.
*/
abstract getTwoFactorAuthenticator(
request: SecretVerificationRequest,
): Promise<TwoFactorAuthenticatorResponse>;
/**
* Gets the email two-factor configuration for the current user.
* Returns the configured email address and enabled status.
* Requires user verification via master password or OTP.
*
* @param request The secret verification request to authorize the operation.
* @returns A promise that resolves to the email two-factor configuration.
*/
abstract getTwoFactorEmail(request: SecretVerificationRequest): Promise<TwoFactorEmailResponse>;
/**
* Gets the Duo two-factor configuration for the current user.
* Returns Duo integration configuration details.
* Requires user verification and an active premium subscription.
*
* @param request The secret verification request to authorize the operation.
* @returns A promise that resolves to the Duo configuration.
*/
abstract getTwoFactorDuo(request: SecretVerificationRequest): Promise<TwoFactorDuoResponse>;
/**
* Gets the Duo two-factor configuration for an organization.
* Returns organization-level Duo integration configuration.
* Requires user verification and organization policy management permissions.
*
* @param organizationId The ID of the organization.
* @param request The secret verification request to authorize the operation.
* @returns A promise that resolves to the organization Duo configuration.
*/
abstract getTwoFactorOrganizationDuo(
organizationId: string,
request: SecretVerificationRequest,
): Promise<TwoFactorDuoResponse>;
/**
* Gets the YubiKey OTP two-factor configuration for the current user.
* Returns configured YubiKey device identifiers (multiple keys supported).
* Requires user verification and an active premium subscription.
*
* @param request The secret verification request to authorize the operation.
* @returns A promise that resolves to the YubiKey configuration.
*/
abstract getTwoFactorYubiKey(
request: SecretVerificationRequest,
): Promise<TwoFactorYubiKeyResponse>;
/**
* Gets the WebAuthn (FIDO2) two-factor configuration for the current user.
* Returns a list of registered WebAuthn credentials with their names and IDs.
* Requires user verification via master password or OTP.
*
* @param request The secret verification request to authorize the operation.
* @returns A promise that resolves to the WebAuthn configuration including registered credentials.
*/
abstract getTwoFactorWebAuthn(
request: SecretVerificationRequest,
): Promise<TwoFactorWebAuthnResponse>;
/**
* Gets a WebAuthn challenge for registering a new WebAuthn credential.
* 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.
*
* @param request The secret verification request to authorize the operation.
* @returns A promise that resolves to the credential creation options containing the challenge.
*/
abstract getTwoFactorWebAuthnChallenge(
request: SecretVerificationRequest,
): Promise<ChallengeResponse>;
/**
* Gets the recovery code configuration for the current user.
* Returns the recovery code that can be used to regain access if other two-factor methods are unavailable.
* The recovery code should be stored securely by the user.
* Requires user verification via master password or OTP.
*
* @param request The secret verification request to authorize the operation.
* @returns A promise that resolves to the recovery code configuration.
*/
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.
*
* @param request The request containing the authenticator configuration and verification token.
* @returns A promise that resolves to the updated authenticator configuration.
*/
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.
*
* @param request The request containing verification credentials to disable the provider.
* @returns A promise that resolves to the updated provider status.
*/
abstract deleteTwoFactorAuthenticator(
request: DisableTwoFactorAuthenticatorRequest,
): Promise<TwoFactorProviderResponse>;
/**
* Enables or updates the email two-factor provider.
* Validates the email verification token sent via postTwoFactorEmailSetup before enabling.
* The token must match the code sent to the specified email address.
*
* @param request The request containing the email configuration and verification token.
* @returns A promise that resolves to the updated email two-factor configuration.
*/
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.
*
* @param request The request containing the Duo integration configuration.
* @returns A promise that resolves to the updated Duo configuration.
*/
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.
*
* @param organizationId The ID of the organization.
* @param request The request containing the Duo integration configuration.
* @returns A promise that resolves to the updated organization Duo configuration.
*/
abstract putTwoFactorOrganizationDuo(
organizationId: string,
request: UpdateTwoFactorDuoRequest,
): Promise<TwoFactorDuoResponse>;
/**
* Enables or updates the YubiKey OTP two-factor provider.
* 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.
* Includes a 2-second delay on validation failure to prevent timing attacks.
*
* @param request The request containing YubiKey device identifiers and test OTPs.
* @returns A promise that resolves to the updated YubiKey configuration.
*/
abstract putTwoFactorYubiKey(
request: UpdateTwoFactorYubikeyOtpRequest,
): Promise<TwoFactorYubiKeyResponse>;
/**
* Registers a new WebAuthn (FIDO2) credential for two-factor authentication.
* 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.
*
* @param request The request containing the WebAuthn credential creation response from the browser.
* @returns A promise that resolves to the updated WebAuthn configuration with the new credential.
*/
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.
*
* @param request The request containing the credential ID to remove.
* @returns A promise that resolves to the updated WebAuthn configuration.
*/
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.
*
* @param request The request specifying which provider type to disable.
* @returns A promise that resolves to the updated provider status.
*/
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.
*
* @param organizationId The ID of the organization.
* @param request The request specifying which provider type to disable.
* @returns A promise that resolves to the updated provider status.
*/
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.
*
* @param request The request containing the email address for two-factor setup.
* @returns A promise that resolves when the verification email has been sent.
*/
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.
*
* @param request The request to send the two-factor code, optionally including SSO or auth request tokens.
* @returns A promise that resolves when the authentication email has been sent.
*/
abstract postTwoFactorEmail(request: TwoFactorEmailRequest): Promise<any>;
}

View File

@@ -48,6 +48,8 @@ import {
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
import { AccountService } from "../auth/abstractions/account.service";
import { TokenService } from "../auth/abstractions/token.service";
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
import { EmailRequest } from "../auth/models/request/email.request";
import { DeviceRequest } from "../auth/models/request/identity-token/device.request";
@@ -59,15 +61,34 @@ import { WebAuthnLoginTokenRequest } from "../auth/models/request/identity-token
import { PasswordHintRequest } from "../auth/models/request/password-hint.request";
import { PasswordlessAuthRequest } from "../auth/models/request/passwordless-auth.request";
import { SecretVerificationRequest } from "../auth/models/request/secret-verification.request";
import { TwoFactorEmailRequest } from "../auth/models/request/two-factor-email.request";
import { TwoFactorProviderRequest } from "../auth/models/request/two-factor-provider.request";
import { UpdateProfileRequest } from "../auth/models/request/update-profile.request";
import { UpdateTwoFactorAuthenticatorRequest } from "../auth/models/request/update-two-factor-authenticator.request";
import { UpdateTwoFactorDuoRequest } from "../auth/models/request/update-two-factor-duo.request";
import { UpdateTwoFactorEmailRequest } from "../auth/models/request/update-two-factor-email.request";
import { UpdateTwoFactorWebAuthnDeleteRequest } from "../auth/models/request/update-two-factor-web-authn-delete.request";
import { UpdateTwoFactorWebAuthnRequest } from "../auth/models/request/update-two-factor-web-authn.request";
import { UpdateTwoFactorYubikeyOtpRequest } from "../auth/models/request/update-two-factor-yubikey-otp.request";
import { ApiKeyResponse } from "../auth/models/response/api-key.response";
import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response";
import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response";
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
import { PreloginResponse } from "../auth/models/response/prelogin.response";
import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response";
import { TwoFactorAuthenticatorResponse } from "../auth/models/response/two-factor-authenticator.response";
import { TwoFactorDuoResponse } from "../auth/models/response/two-factor-duo.response";
import { TwoFactorEmailResponse } from "../auth/models/response/two-factor-email.response";
import { TwoFactorProviderResponse } from "../auth/models/response/two-factor-provider.response";
import { TwoFactorRecoverResponse } from "../auth/models/response/two-factor-recover.response";
import {
ChallengeResponse,
TwoFactorWebAuthnResponse,
} from "../auth/models/response/two-factor-web-authn.response";
import { TwoFactorYubiKeyResponse } from "../auth/models/response/two-factor-yubi-key.response";
import { BitPayInvoiceRequest } from "../billing/models/request/bit-pay-invoice.request";
import { BillingHistoryResponse } from "../billing/models/response/billing-history.response";
import { PaymentResponse } from "../billing/models/response/payment.response";
@@ -788,6 +809,205 @@ export class ApiService implements ApiServiceAbstraction {
return new SyncResponse(r);
}
// Two-factor APIs
async getTwoFactorProviders(): Promise<ListResponse<TwoFactorProviderResponse>> {
const r = await this.send("GET", "/two-factor", null, true, true);
return new ListResponse(r, TwoFactorProviderResponse);
}
async getTwoFactorOrganizationProviders(
organizationId: string,
): Promise<ListResponse<TwoFactorProviderResponse>> {
const r = await this.send(
"GET",
"/organizations/" + organizationId + "/two-factor",
null,
true,
true,
);
return new ListResponse(r, TwoFactorProviderResponse);
}
async getTwoFactorAuthenticator(
request: SecretVerificationRequest,
): Promise<TwoFactorAuthenticatorResponse> {
const r = await this.send("POST", "/two-factor/get-authenticator", request, true, true);
return new TwoFactorAuthenticatorResponse(r);
}
async getTwoFactorEmail(request: SecretVerificationRequest): Promise<TwoFactorEmailResponse> {
const r = await this.send("POST", "/two-factor/get-email", request, true, true);
return new TwoFactorEmailResponse(r);
}
async getTwoFactorDuo(request: SecretVerificationRequest): Promise<TwoFactorDuoResponse> {
const r = await this.send("POST", "/two-factor/get-duo", request, true, true);
return new TwoFactorDuoResponse(r);
}
async getTwoFactorOrganizationDuo(
organizationId: string,
request: SecretVerificationRequest,
): Promise<TwoFactorDuoResponse> {
const r = await this.send(
"POST",
"/organizations/" + organizationId + "/two-factor/get-duo",
request,
true,
true,
);
return new TwoFactorDuoResponse(r);
}
async getTwoFactorYubiKey(request: SecretVerificationRequest): Promise<TwoFactorYubiKeyResponse> {
const r = await this.send("POST", "/two-factor/get-yubikey", request, true, true);
return new TwoFactorYubiKeyResponse(r);
}
async getTwoFactorWebAuthn(
request: SecretVerificationRequest,
): Promise<TwoFactorWebAuthnResponse> {
const r = await this.send("POST", "/two-factor/get-webauthn", request, true, true);
return new TwoFactorWebAuthnResponse(r);
}
async getTwoFactorWebAuthnChallenge(
request: SecretVerificationRequest,
): Promise<ChallengeResponse> {
const r = await this.send("POST", "/two-factor/get-webauthn-challenge", request, true, true);
return new ChallengeResponse(r);
}
async getTwoFactorRecover(request: SecretVerificationRequest): Promise<TwoFactorRecoverResponse> {
const r = await this.send("POST", "/two-factor/get-recover", request, true, true);
return new TwoFactorRecoverResponse(r);
}
async putTwoFactorAuthenticator(
request: UpdateTwoFactorAuthenticatorRequest,
): Promise<TwoFactorAuthenticatorResponse> {
const r = await this.send("PUT", "/two-factor/authenticator", request, true, true);
return new TwoFactorAuthenticatorResponse(r);
}
async deleteTwoFactorAuthenticator(
request: DisableTwoFactorAuthenticatorRequest,
): Promise<TwoFactorProviderResponse> {
const r = await this.send("DELETE", "/two-factor/authenticator", request, true, true);
return new TwoFactorProviderResponse(r);
}
async putTwoFactorEmail(request: UpdateTwoFactorEmailRequest): Promise<TwoFactorEmailResponse> {
const r = await this.send("PUT", "/two-factor/email", request, true, true);
return new TwoFactorEmailResponse(r);
}
async putTwoFactorDuo(request: UpdateTwoFactorDuoRequest): Promise<TwoFactorDuoResponse> {
const r = await this.send("PUT", "/two-factor/duo", request, true, true);
return new TwoFactorDuoResponse(r);
}
async putTwoFactorOrganizationDuo(
organizationId: string,
request: UpdateTwoFactorDuoRequest,
): Promise<TwoFactorDuoResponse> {
const r = await this.send(
"PUT",
"/organizations/" + organizationId + "/two-factor/duo",
request,
true,
true,
);
return new TwoFactorDuoResponse(r);
}
async putTwoFactorYubiKey(
request: UpdateTwoFactorYubikeyOtpRequest,
): Promise<TwoFactorYubiKeyResponse> {
const r = await this.send("PUT", "/two-factor/yubikey", request, true, true);
return new TwoFactorYubiKeyResponse(r);
}
async putTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnRequest,
): Promise<TwoFactorWebAuthnResponse> {
const response = request.deviceResponse.response as AuthenticatorAttestationResponse;
const data: any = Object.assign({}, request);
data.deviceResponse = {
id: request.deviceResponse.id,
rawId: btoa(request.deviceResponse.id),
type: request.deviceResponse.type,
extensions: request.deviceResponse.getClientExtensionResults(),
response: {
AttestationObject: Utils.fromBufferToB64(response.attestationObject),
clientDataJson: Utils.fromBufferToB64(response.clientDataJSON),
},
};
const r = await this.send("PUT", "/two-factor/webauthn", data, true, true);
return new TwoFactorWebAuthnResponse(r);
}
async deleteTwoFactorWebAuthn(
request: UpdateTwoFactorWebAuthnDeleteRequest,
): Promise<TwoFactorWebAuthnResponse> {
const r = await this.send("DELETE", "/two-factor/webauthn", request, true, true);
return new TwoFactorWebAuthnResponse(r);
}
async putTwoFactorDisable(request: TwoFactorProviderRequest): Promise<TwoFactorProviderResponse> {
const r = await this.send("PUT", "/two-factor/disable", request, true, true);
return new TwoFactorProviderResponse(r);
}
async putTwoFactorOrganizationDisable(
organizationId: string,
request: TwoFactorProviderRequest,
): Promise<TwoFactorProviderResponse> {
const r = await this.send(
"PUT",
"/organizations/" + organizationId + "/two-factor/disable",
request,
true,
true,
);
return new TwoFactorProviderResponse(r);
}
postTwoFactorEmailSetup(request: TwoFactorEmailRequest): Promise<any> {
return this.send("POST", "/two-factor/send-email", request, true, false);
}
postTwoFactorEmail(request: TwoFactorEmailRequest): Promise<any> {
return this.send("POST", "/two-factor/send-email-login", request, false, false);
}
async getDeviceVerificationSettings(): Promise<DeviceVerificationResponse> {
const r = await this.send(
"GET",
"/two-factor/get-device-verification-settings",
null,
true,
true,
);
return new DeviceVerificationResponse(r);
}
async putDeviceVerificationSettings(
request: DeviceVerificationRequest,
): Promise<DeviceVerificationResponse> {
const r = await this.send(
"PUT",
"/two-factor/device-verification-settings",
request,
true,
true,
);
return new DeviceVerificationResponse(r);
}
// Organization APIs
async getCloudCommunicationsEnabled(): Promise<boolean> {