mirror of
https://github.com/bitwarden/browser
synced 2026-02-28 10:33:31 +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:
@@ -0,0 +1,697 @@
|
||||
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
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,272 @@
|
||||
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 "../abstractions/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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
2
libs/common/src/auth/two-factor/services/index.ts
Normal file
2
libs/common/src/auth/two-factor/services/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./default-two-factor-api.service";
|
||||
export * from "./default-two-factor.service";
|
||||
Reference in New Issue
Block a user