1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-29 06:33:40 +00:00

Merged with master and fixed conflicts

This commit is contained in:
gbubemismith
2023-05-08 12:16:25 -04:00
1131 changed files with 52396 additions and 51717 deletions

View File

@@ -1,7 +1,7 @@
/**
* The inbuilt toEqual() matcher will always return TRUE when provided with 2 ArrayBuffers.
* This is because an ArrayBuffer must be wrapped in a new Uint8Array to be accessible.
* This custom matcher will automatically instantiate a new Uint8Array on the recieved value
* This custom matcher will automatically instantiate a new Uint8Array on the received value
* (and optionally, the expected value) and then call toEqual() on the resulting Uint8Arrays.
*/
export const toEqualBuffer: jest.CustomMatcher = function (

View File

@@ -1,3 +1,5 @@
import * as path from "path";
import { Utils } from "@bitwarden/common/misc/utils";
describe("Utils Service", () => {
@@ -343,7 +345,17 @@ describe("Utils Service", () => {
it("removes multiple encoded traversals", () => {
expect(
Utils.normalizePath("api/sends/access/..%2f..%2f..%2fapi%2fsends%2faccess%2fsendkey")
).toBe("api/sends/access/sendkey");
).toBe(path.normalize("api/sends/access/sendkey"));
});
});
describe("getUrl", () => {
it("assumes a http protocol if no protocol is specified", () => {
const urlString = "www.exampleapp.com.au:4000";
const actual = Utils.getUrl(urlString);
expect(actual.protocol).toBe("http:");
});
});
});

View File

@@ -4,7 +4,7 @@ import { mock, MockProxy } from "jest-mock-extended";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service";
import { EncryptionType } from "@bitwarden/common/enums/encryptionType";
import { EncryptionType } from "@bitwarden/common/enums";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { ContainerService } from "@bitwarden/common/services/container.service";
@@ -25,6 +25,16 @@ describe("EncString", () => {
});
});
describe("isSerializedEncString", () => {
it("is true if valid", () => {
expect(EncString.isSerializedEncString("3.data")).toBe(true);
});
it("is false if invalid", () => {
expect(EncString.isSerializedEncString("3.data|test")).toBe(false);
});
});
describe("parse existing", () => {
it("valid", () => {
const encString = new EncString("3.data");
@@ -89,6 +99,16 @@ describe("EncString", () => {
});
});
describe("isSerializedEncString", () => {
it("is true if valid", () => {
expect(EncString.isSerializedEncString("0.iv|data")).toBe(true);
});
it("is false if invalid", () => {
expect(EncString.isSerializedEncString("0.iv|data|mac")).toBe(false);
});
});
describe("parse existing", () => {
it("valid", () => {
const encString = new EncString("0.iv|data");
@@ -125,6 +145,16 @@ describe("EncString", () => {
});
});
describe("isSerializedEncString", () => {
it("is true if valid", () => {
expect(EncString.isSerializedEncString("2.iv|data|mac")).toBe(true);
});
it("is false if invalid", () => {
expect(EncString.isSerializedEncString("2.iv|data")).toBe(false);
});
});
it("valid", () => {
const encString = new EncString("2.iv|data|mac");

View File

@@ -1,4 +1,4 @@
import { EncryptionType } from "@bitwarden/common/enums/encryptionType";
import { EncryptionType } from "@bitwarden/common/enums";
import { EncArrayBuffer } from "@bitwarden/common/models/domain/enc-array-buffer";
import { makeStaticByteArray } from "../../utils";

View File

@@ -1,4 +1,4 @@
import { EncryptionType } from "@bitwarden/common/enums/encryptionType";
import { EncryptionType } from "@bitwarden/common/enums";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { makeStaticByteArray } from "../../utils";

View File

@@ -2,12 +2,13 @@ import { mockReset, mock } from "jest-mock-extended";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { EncryptionType } from "@bitwarden/common/enums/encryptionType";
import { EncryptionType } from "@bitwarden/common/enums";
import { EncArrayBuffer } from "@bitwarden/common/models/domain/enc-array-buffer";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { EncryptServiceImplementation } from "@bitwarden/common/services/cryptography/encrypt.service.implementation";
import { CsprngArray } from "../../src/types/csprng";
import { makeStaticByteArray } from "../utils";
describe("EncryptService", () => {
@@ -37,7 +38,9 @@ describe("EncryptService", () => {
describe("encrypts data", () => {
beforeEach(() => {
cryptoFunctionService.randomBytes.calledWith(16).mockResolvedValueOnce(iv.buffer);
cryptoFunctionService.randomBytes
.calledWith(16)
.mockResolvedValueOnce(iv.buffer as CsprngArray);
cryptoFunctionService.aesEncrypt.mockResolvedValue(encryptedData.buffer);
});

View File

@@ -1,292 +0,0 @@
// eslint-disable-next-line no-restricted-imports
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { KdfType, DEFAULT_PBKDF2_ITERATIONS } from "@bitwarden/common/enums/kdfType";
import { Utils } from "@bitwarden/common/misc/utils";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { CipherWithIdExport as CipherExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
import { ExportService } from "@bitwarden/common/services/export.service";
import { StateService } from "@bitwarden/common/services/state.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { Folder } from "@bitwarden/common/vault/models/domain/folder";
import { Login } from "@bitwarden/common/vault/models/domain/login";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { BuildTestObject, GetUniqueString } from "../utils";
const UserCipherViews = [
generateCipherView(false),
generateCipherView(false),
generateCipherView(true),
];
const UserCipherDomains = [
generateCipherDomain(false),
generateCipherDomain(false),
generateCipherDomain(true),
];
const UserFolderViews = [generateFolderView(), generateFolderView()];
const UserFolders = [generateFolder(), generateFolder()];
function generateCipherView(deleted: boolean) {
return BuildTestObject(
{
id: GetUniqueString("id"),
notes: GetUniqueString("notes"),
type: CipherType.Login,
login: BuildTestObject<LoginView>(
{
username: GetUniqueString("username"),
password: GetUniqueString("password"),
},
LoginView
),
collectionIds: null,
deletedDate: deleted ? new Date() : null,
},
CipherView
);
}
function generateCipherDomain(deleted: boolean) {
return BuildTestObject(
{
id: GetUniqueString("id"),
notes: new EncString(GetUniqueString("notes")),
type: CipherType.Login,
login: BuildTestObject<Login>(
{
username: new EncString(GetUniqueString("username")),
password: new EncString(GetUniqueString("password")),
},
Login
),
collectionIds: null,
deletedDate: deleted ? new Date() : null,
},
Cipher
);
}
function generateFolderView() {
return BuildTestObject(
{
id: GetUniqueString("id"),
name: GetUniqueString("name"),
revisionDate: new Date(),
},
FolderView
);
}
function generateFolder() {
const actual = Folder.fromJSON({
revisionDate: new Date("2022-08-04T01:06:40.441Z").toISOString(),
name: "name",
id: "id",
});
return actual;
}
function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string) {
const actual = JSON.stringify(JSON.parse(jsonResult).items);
const items: CipherExport[] = [];
ciphers.forEach((c: CipherView | Cipher) => {
const item = new CipherExport();
item.build(c);
items.push(item);
});
expect(actual).toEqual(JSON.stringify(items));
}
function expectEqualFolderViews(folderviews: FolderView[] | Folder[], jsonResult: string) {
const actual = JSON.stringify(JSON.parse(jsonResult).folders);
const folders: FolderResponse[] = [];
folderviews.forEach((c) => {
const folder = new FolderResponse();
folder.id = c.id;
folder.name = c.name.toString();
folders.push(folder);
});
expect(actual.length).toBeGreaterThan(0);
expect(actual).toEqual(JSON.stringify(folders));
}
function expectEqualFolders(folders: Folder[], jsonResult: string) {
const actual = JSON.stringify(JSON.parse(jsonResult).folders);
const items: Folder[] = [];
folders.forEach((c) => {
const item = new Folder();
item.id = c.id;
item.name = c.name;
items.push(item);
});
expect(actual.length).toBeGreaterThan(0);
expect(actual).toEqual(JSON.stringify(items));
}
describe("ExportService", () => {
let exportService: ExportService;
let apiService: SubstituteOf<ApiService>;
let cryptoFunctionService: SubstituteOf<CryptoFunctionService>;
let cipherService: SubstituteOf<CipherService>;
let folderService: SubstituteOf<FolderService>;
let cryptoService: SubstituteOf<CryptoService>;
let stateService: SubstituteOf<StateService>;
beforeEach(() => {
apiService = Substitute.for<ApiService>();
cryptoFunctionService = Substitute.for<CryptoFunctionService>();
cipherService = Substitute.for<CipherService>();
folderService = Substitute.for<FolderService>();
cryptoService = Substitute.for<CryptoService>();
stateService = Substitute.for<StateService>();
folderService.getAllDecryptedFromState().resolves(UserFolderViews);
folderService.getAllFromState().resolves(UserFolders);
stateService.getKdfType().resolves(KdfType.PBKDF2_SHA256);
stateService.getKdfConfig().resolves(new KdfConfig(DEFAULT_PBKDF2_ITERATIONS));
exportService = new ExportService(
folderService,
cipherService,
apiService,
cryptoService,
cryptoFunctionService,
stateService
);
});
it("exports unecrypted user ciphers", async () => {
cipherService.getAllDecrypted().resolves(UserCipherViews.slice(0, 1));
const actual = await exportService.getExport("json");
expectEqualCiphers(UserCipherViews.slice(0, 1), actual);
});
it("exports encrypted json user ciphers", async () => {
cipherService.getAll().resolves(UserCipherDomains.slice(0, 1));
const actual = await exportService.getExport("encrypted_json");
expectEqualCiphers(UserCipherDomains.slice(0, 1), actual);
});
it("does not unecrypted export trashed user items", async () => {
cipherService.getAllDecrypted().resolves(UserCipherViews);
const actual = await exportService.getExport("json");
expectEqualCiphers(UserCipherViews.slice(0, 2), actual);
});
it("does not encrypted export trashed user items", async () => {
cipherService.getAll().resolves(UserCipherDomains);
const actual = await exportService.getExport("encrypted_json");
expectEqualCiphers(UserCipherDomains.slice(0, 2), actual);
});
describe("password protected export", () => {
let exportString: string;
let exportObject: any;
let mac: SubstituteOf<EncString>;
let data: SubstituteOf<EncString>;
const password = "password";
const salt = "salt";
describe("export json object", () => {
beforeEach(async () => {
mac = Substitute.for<EncString>();
data = Substitute.for<EncString>();
mac.encryptedString.returns("mac");
data.encryptedString.returns("encData");
jest.spyOn(Utils, "fromBufferToB64").mockReturnValue(salt);
cipherService.getAllDecrypted().resolves(UserCipherViews.slice(0, 1));
exportString = await exportService.getPasswordProtectedExport(password);
exportObject = JSON.parse(exportString);
});
it("specifies it is encrypted", () => {
expect(exportObject.encrypted).toBe(true);
});
it("specifies it's password protected", () => {
expect(exportObject.passwordProtected).toBe(true);
});
it("specifies salt", () => {
expect(exportObject.salt).toEqual("salt");
});
it("specifies kdfIterations", () => {
expect(exportObject.kdfIterations).toEqual(DEFAULT_PBKDF2_ITERATIONS);
});
it("has kdfType", () => {
expect(exportObject.kdfType).toEqual(KdfType.PBKDF2_SHA256);
});
it("has a mac property", async () => {
cryptoService.encrypt(Arg.any(), Arg.any()).resolves(mac);
exportString = await exportService.getPasswordProtectedExport(password);
exportObject = JSON.parse(exportString);
expect(exportObject.encKeyValidation_DO_NOT_EDIT).toEqual(mac.encryptedString);
});
it("has data property", async () => {
cryptoService.encrypt(Arg.any(), Arg.any()).resolves(data);
exportString = await exportService.getPasswordProtectedExport(password);
exportObject = JSON.parse(exportString);
expect(exportObject.data).toEqual(data.encryptedString);
});
it("encrypts the data property", async () => {
const unencrypted = await exportService.getExport();
expect(exportObject.data).not.toEqual(unencrypted);
});
});
});
it("exported unencrypted object contains folders", async () => {
cipherService.getAllDecrypted().resolves(UserCipherViews.slice(0, 1));
await folderService.getAllDecryptedFromState();
const actual = await exportService.getExport("json");
expectEqualFolderViews(UserFolderViews, actual);
});
it("exported encrypted json contains folders", async () => {
cipherService.getAll().resolves(UserCipherDomains.slice(0, 1));
await folderService.getAllFromState();
const actual = await exportService.getExport("encrypted_json");
expectEqualFolders(UserFolders, actual);
});
});
export class FolderResponse {
id: string = null;
name: string = null;
}

View File

@@ -1,12 +1,14 @@
import { mock, mockReset } from "jest-mock-extended";
import { mock } from "jest-mock-extended";
import { lastValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/abstractions/organization-domain/responses/organization-domain-sso-details.response";
import { OrganizationDomainResponse } from "@bitwarden/common/abstractions/organization-domain/responses/organization-domain.response";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrgDomainApiService } from "@bitwarden/common/services/organization-domain/org-domain-api.service";
import { OrgDomainService } from "@bitwarden/common/services/organization-domain/org-domain.service";
import { OrganizationDomainSsoDetailsRequest } from "@bitwarden/common/services/organization-domain/requests/organization-domain-sso-details.request";
const mockedGetAllByOrgIdResponse: any = {
data: [
@@ -66,6 +68,18 @@ const mockedOrgDomainServerResponse = {
const mockedOrgDomainResponse = new OrganizationDomainResponse(mockedOrgDomainServerResponse);
const mockedOrganizationDomainSsoDetailsServerResponse = {
id: "fake-guid",
organizationIdentifier: "fake-org-identifier",
ssoAvailable: true,
domainName: "fake-domain-name",
verifiedDate: "2022-12-16T21:36:28.68Z",
};
const mockedOrganizationDomainSsoDetailsResponse = new OrganizationDomainSsoDetailsResponse(
mockedOrganizationDomainSsoDetailsServerResponse
);
describe("Org Domain API Service", () => {
let orgDomainApiService: OrgDomainApiService;
@@ -78,7 +92,7 @@ describe("Org Domain API Service", () => {
beforeEach(() => {
orgDomainService = new OrgDomainService(platformUtilService, i18nService);
mockReset(apiService);
jest.resetAllMocks();
orgDomainApiService = new OrgDomainApiService(orgDomainService, apiService);
});
@@ -168,6 +182,20 @@ describe("Org Domain API Service", () => {
});
});
// TODO: add Get Domain SSO method: Retrieves SSO provider information given a domain name
// when added on back end
it("getClaimedOrgDomainByEmail should call ApiService.send with correct parameters and return response", async () => {
const email = "test@example.com";
apiService.send.mockResolvedValue(mockedOrganizationDomainSsoDetailsServerResponse);
const result = await orgDomainApiService.getClaimedOrgDomainByEmail(email);
expect(apiService.send).toHaveBeenCalledWith(
"POST",
"/organizations/domain/sso/details",
new OrganizationDomainSsoDetailsRequest(email),
false, //anonymous
true
);
expect(result).toEqual(mockedOrganizationDomainSsoDetailsResponse);
});
});

View File

@@ -160,7 +160,7 @@ describe("Organization Service", () => {
});
function organizationData(id: string, name: string) {
const data = new OrganizationData({} as any);
const data = new OrganizationData({} as any, {} as any);
data.id = id;
data.name = name;
data.identifier = "test";

View File

@@ -5,8 +5,7 @@ import { BehaviorSubject, firstValueFrom } from "rxjs";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EncryptService } from "@bitwarden/common/abstractions/encrypt.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums/organization-user-status-type";
import { PolicyType } from "@bitwarden/common/admin-console/enums/policy-type";
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data";
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
@@ -162,6 +161,7 @@ describe("PolicyService", () => {
requireNumbers: false,
requireSpecial: false,
requireUpper: true,
enforceOnLogin: false,
});
});
@@ -201,6 +201,7 @@ describe("PolicyService", () => {
requireNumbers: false,
requireSpecial: false,
requireUpper: false,
enforceOnLogin: false,
});
});
});
@@ -402,7 +403,7 @@ describe("PolicyService", () => {
status: OrganizationUserStatusType,
managePolicies: boolean
) {
const organizationData = new OrganizationData({} as any);
const organizationData = new OrganizationData({} as any, {} as any);
organizationData.id = id;
organizationData.enabled = enabled;
organizationData.usePolicies = usePolicies;

View File

@@ -17,6 +17,12 @@ describe("SettingsService", () => {
let activeAccount: BehaviorSubject<string>;
let activeAccountUnlocked: BehaviorSubject<boolean>;
const mockEquivalentDomains = [
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
["example.co.uk", "exampleapp.co.uk"],
];
beforeEach(() => {
cryptoService = Substitute.for();
encryptService = Substitute.for();
@@ -24,7 +30,7 @@ describe("SettingsService", () => {
activeAccount = new BehaviorSubject("123");
activeAccountUnlocked = new BehaviorSubject(true);
stateService.getSettings().resolves({ equivalentDomains: [["test"], ["domains"]] });
stateService.getSettings().resolves({ equivalentDomains: mockEquivalentDomains });
stateService.activeAccount$.returns(activeAccount);
stateService.activeAccountUnlocked$.returns(activeAccountUnlocked);
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
@@ -38,12 +44,21 @@ describe("SettingsService", () => {
});
describe("getEquivalentDomains", () => {
it("returns value", async () => {
const result = await firstValueFrom(settingsService.settings$);
it("returns all equivalent domains for a URL", async () => {
const actual = settingsService.getEquivalentDomains("example.co.uk");
const expected = new Set([
"example.com",
"exampleapp.com",
"example.co.uk",
"ejemplo.es",
"exampleapp.co.uk",
]);
expect(actual).toEqual(expected);
});
expect(result).toEqual({
equivalentDomains: [["test"], ["domains"]],
});
it("returns an empty set if there are no equivalent domains", () => {
const actual = settingsService.getEquivalentDomains("asdf");
expect(actual).toEqual(new Set());
});
});

View File

@@ -1,8 +1,9 @@
// eslint-disable-next-line no-restricted-imports
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { MockProxy, any, mock } from "jest-mock-extended";
import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service";
import { StateVersion } from "@bitwarden/common/enums/stateVersion";
import { StateVersion } from "@bitwarden/common/enums";
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
import { Account } from "@bitwarden/common/models/domain/account";
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
@@ -14,14 +15,14 @@ const userId = "USER_ID";
// so that we don't accidentally run all following migrations as well
describe("State Migration Service", () => {
let storageService: SubstituteOf<AbstractStorageService>;
let storageService: MockProxy<AbstractStorageService>;
let secureStorageService: SubstituteOf<AbstractStorageService>;
let stateFactory: SubstituteOf<StateFactory>;
let stateMigrationService: StateMigrationService;
beforeEach(() => {
storageService = Substitute.for<AbstractStorageService>();
storageService = mock();
secureStorageService = Substitute.for<AbstractStorageService>();
stateFactory = Substitute.for<StateFactory>();
@@ -32,14 +33,18 @@ describe("State Migration Service", () => {
);
});
afterEach(() => {
jest.resetAllMocks();
});
describe("StateVersion 3 to 4 migration", () => {
beforeEach(() => {
const globalVersion3: Partial<GlobalState> = {
stateVersion: StateVersion.Three,
};
storageService.get("global", Arg.any()).resolves(globalVersion3);
storageService.get("authenticatedAccounts", Arg.any()).resolves([userId]);
storageService.get.calledWith("global", any()).mockResolvedValue(globalVersion3);
storageService.get.calledWith("authenticatedAccounts", any()).mockResolvedValue([userId]);
});
it("clears everBeenUnlocked", async () => {
@@ -57,7 +62,7 @@ describe("State Migration Service", () => {
lastSync: "LAST_SYNC",
userId: userId,
usesKeyConnector: false,
forcePasswordReset: false,
forcePasswordResetReason: null,
},
};
@@ -68,21 +73,23 @@ describe("State Migration Service", () => {
};
delete expectedAccountVersion4.profile.everBeenUnlocked;
storageService.get(userId, Arg.any()).resolves(accountVersion3);
storageService.get.calledWith(userId, any()).mockResolvedValue(accountVersion3);
await (stateMigrationService as any).migrateStateFrom3To4();
storageService.received(1).save(userId, expectedAccountVersion4, Arg.any());
expect(storageService.save).toHaveBeenCalledTimes(2);
expect(storageService.save).toHaveBeenCalledWith(userId, expectedAccountVersion4, any());
});
it("updates StateVersion number", async () => {
await (stateMigrationService as any).migrateStateFrom3To4();
storageService.received(1).save(
expect(storageService.save).toHaveBeenCalledWith(
"global",
Arg.is((globals: GlobalState) => globals.stateVersion === StateVersion.Four),
Arg.any()
{ stateVersion: StateVersion.Four },
any()
);
expect(storageService.save).toHaveBeenCalledTimes(1);
});
});
@@ -144,4 +151,65 @@ describe("State Migration Service", () => {
expect(migratedAccount.keys.legacyEtmKey).toBeUndefined();
});
});
describe("StateVersion 6 to 7 migration", () => {
it("should delete global.noAutoPromptBiometrics value", async () => {
storageService.get
.calledWith("global", any())
.mockResolvedValue({ stateVersion: StateVersion.Six, noAutoPromptBiometrics: true });
storageService.get.calledWith("authenticatedAccounts", any()).mockResolvedValue([]);
await stateMigrationService.migrate();
expect(storageService.save).toHaveBeenCalledWith(
"global",
{
stateVersion: StateVersion.Seven,
},
any()
);
});
it("should call migrateStateFrom6To7 on each account", async () => {
const accountVersion6 = new Account({
otherStuff: "other stuff",
} as any);
storageService.get
.calledWith("global", any())
.mockResolvedValue({ stateVersion: StateVersion.Six, noAutoPromptBiometrics: true });
storageService.get.calledWith("authenticatedAccounts", any()).mockResolvedValue([userId]);
storageService.get.calledWith(userId, any()).mockResolvedValue(accountVersion6);
const migrateSpy = jest.fn();
(stateMigrationService as any).migrateAccountFrom6To7 = migrateSpy;
await stateMigrationService.migrate();
expect(migrateSpy).toHaveBeenCalledWith(true, accountVersion6);
});
it("should update account.settings.disableAutoBiometricsPrompt value if global is no prompt", async () => {
const result = await (stateMigrationService as any).migrateAccountFrom6To7(true, {
otherStuff: "other stuff",
});
expect(result).toEqual({
otherStuff: "other stuff",
settings: {
disableAutoBiometricsPrompt: true,
},
});
});
it("should not update account.settings.disableAutoBiometricsPrompt value if global auto prompt is enabled", async () => {
const result = await (stateMigrationService as any).migrateAccountFrom6To7(false, {
otherStuff: "other stuff",
});
expect(result).toEqual({
otherStuff: "other stuff",
});
});
});
});

View File

@@ -1,4 +1,4 @@
import { OrganizationConnectionType } from "../admin-console/enums/organization-connection-type";
import { OrganizationConnectionType } from "../admin-console/enums";
import { CollectionRequest } from "../admin-console/models/request/collection.request";
import { OrganizationConnectionRequest } from "../admin-console/models/request/organization-connection.request";
import { OrganizationSponsorshipCreateRequest } from "../admin-console/models/request/organization/organization-sponsorship-create.request";
@@ -80,6 +80,7 @@ import { IdentityCaptchaResponse } from "../auth/models/response/identity-captch
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 { MasterPasswordPolicyResponse } from "../auth/models/response/master-password-policy.response";
import { PreloginResponse } from "../auth/models/response/prelogin.response";
import { RegisterResponse } from "../auth/models/response/register.response";
import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response";
@@ -187,7 +188,9 @@ export abstract class ApiService {
postAccountKeys: (request: KeysRequest) => Promise<any>;
postAccountVerifyEmail: () => Promise<any>;
postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise<any>;
postAccountVerifyPassword: (request: SecretVerificationRequest) => Promise<any>;
postAccountVerifyPassword: (
request: SecretVerificationRequest
) => Promise<MasterPasswordPolicyResponse>;
postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise<any>;
postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise<any>;
postAccountKdf: (request: KdfRequest) => Promise<any>;

View File

@@ -1,7 +1,13 @@
import { Observable } from "rxjs";
import { FeatureFlag } from "../../enums/feature-flag.enum";
import { ServerConfig } from "./server-config";
export abstract class ConfigServiceAbstraction {
serverConfig$: Observable<ServerConfig | null>;
fetchServerConfig: () => Promise<ServerConfig>;
getFeatureFlagBool: (key: FeatureFlag, defaultValue?: boolean) => Promise<boolean>;
getFeatureFlagString: (key: FeatureFlag, defaultValue?: string) => Promise<string>;
getFeatureFlagNumber: (key: FeatureFlag, defaultValue?: number) => Promise<number>;
}

View File

@@ -15,6 +15,7 @@ export class ServerConfig {
server?: ThirdPartyServerConfigData;
environment?: EnvironmentServerConfigData;
utcDate: Date;
featureStates: { [key: string]: string } = {};
constructor(serverConfigData: ServerConfigData) {
this.version = serverConfigData.version;
@@ -22,6 +23,7 @@ export class ServerConfig {
this.server = serverConfigData.server;
this.utcDate = new Date(serverConfigData.utcDate);
this.environment = serverConfigData.environment;
this.featureStates = serverConfigData.featureStates;
if (this.server?.name == null && this.server?.url == null) {
this.server = null;

View File

@@ -2,9 +2,7 @@ import { ProfileOrganizationResponse } from "../admin-console/models/response/pr
import { ProfileProviderOrganizationResponse } from "../admin-console/models/response/profile-provider-organization.response";
import { ProfileProviderResponse } from "../admin-console/models/response/profile-provider.response";
import { KdfConfig } from "../auth/models/domain/kdf-config";
import { HashPurpose } from "../enums/hashPurpose";
import { KdfType } from "../enums/kdfType";
import { KeySuffixOptions } from "../enums/keySuffixOptions";
import { HashPurpose, KdfType, KeySuffixOptions } from "../enums";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";

View File

@@ -1,5 +1,6 @@
import { DecryptParameters } from "../models/domain/decrypt-parameters";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
import { CsprngArray } from "../types/csprng";
export abstract class CryptoFunctionService {
pbkdf2: (
@@ -65,5 +66,5 @@ export abstract class CryptoFunctionService {
) => Promise<ArrayBuffer>;
rsaExtractPublicKey: (privateKey: ArrayBuffer) => Promise<ArrayBuffer>;
rsaGenerateKeyPair: (length: 1024 | 2048 | 4096) => Promise<[ArrayBuffer, ArrayBuffer]>;
randomBytes: (length: number) => Promise<ArrayBuffer>;
randomBytes: (length: number) => Promise<CsprngArray>;
}

View File

@@ -1,4 +1,4 @@
import { EventType } from "../../enums/eventType";
import { EventType } from "../../enums";
export abstract class EventCollectionService {
collect: (

View File

@@ -1,11 +0,0 @@
import { EventView } from "../models/view/event.view";
export type ExportFormat = "csv" | "json" | "encrypted_json";
export abstract class ExportService {
getExport: (format?: ExportFormat, organizationId?: string) => Promise<string>;
getPasswordProtectedExport: (password: string, organizationId?: string) => Promise<string>;
getOrganizationExport: (organizationId: string, format?: ExportFormat) => Promise<string>;
getEventExport: (events: EventView[]) => Promise<string>;
getFileName: (prefix?: string, extension?: string) => string;
}

View File

@@ -1,4 +1,4 @@
import { FileUploadType } from "../../enums/fileUploadType";
import { FileUploadType } from "../../enums";
import { EncArrayBuffer } from "../../models/domain/enc-array-buffer";
import { EncString } from "../../models/domain/enc-string";

View File

@@ -1,4 +1,4 @@
import { LogLevelType } from "../enums/logLevelType";
import { LogLevelType } from "../enums";
export abstract class LogService {
debug: (message: string) => void;

View File

@@ -1,6 +1,7 @@
import { ListResponse } from "../../models/response/list.response";
import {
OrganizationUserAcceptInitRequest,
OrganizationUserAcceptRequest,
OrganizationUserBulkConfirmRequest,
OrganizationUserConfirmRequest,
@@ -14,7 +15,7 @@ import {
OrganizationUserBulkPublicKeyResponse,
OrganizationUserBulkResponse,
OrganizationUserDetailsResponse,
OrganizationUserResetPasswordDetailsReponse,
OrganizationUserResetPasswordDetailsResponse,
OrganizationUserUserDetailsResponse,
} from "./responses";
@@ -64,7 +65,7 @@ export abstract class OrganizationUserService {
abstract getOrganizationUserResetPasswordDetails(
organizationId: string,
id: string
): Promise<OrganizationUserResetPasswordDetailsReponse>;
): Promise<OrganizationUserResetPasswordDetailsResponse>;
/**
* Create new organization user invite(s) for the specified organization
@@ -94,6 +95,20 @@ export abstract class OrganizationUserService {
ids: string[]
): Promise<ListResponse<OrganizationUserBulkResponse>>;
/**
* Accept an invitation to initialize and join an organization created via the Admin Portal **only**.
* This is only used once for the initial Owner, because it also creates the organization's encryption keys.
* This should not be used for organizations created via the Web client.
* @param organizationId - Identifier for the organization to accept
* @param id - Organization user identifier
* @param request - Request details for accepting the invitation
*/
abstract postOrganizationUserAcceptInit(
organizationId: string,
id: string,
request: OrganizationUserAcceptInitRequest
): Promise<void>;
/**
* Accept an organization user invitation
* @param organizationId - Identifier for the organization to accept

View File

@@ -1,3 +1,4 @@
export * from "./organization-user-accept-init.request";
export * from "./organization-user-accept.request";
export * from "./organization-user-bulk-confirm.request";
export * from "./organization-user-confirm.request";

View File

@@ -0,0 +1,8 @@
import { OrganizationKeysRequest } from "../../../admin-console/models/request/organization-keys.request";
export class OrganizationUserAcceptInitRequest {
token: string;
key: string;
keys: OrganizationKeysRequest;
collectionName: string;
}

View File

@@ -1,4 +1,4 @@
import { OrganizationUserType } from "../../../admin-console/enums/organization-user-type";
import { OrganizationUserType } from "../../../admin-console/enums";
import { PermissionsApi } from "../../../admin-console/models/api/permissions.api";
import { SelectionReadOnlyRequest } from "../../../admin-console/models/request/selection-read-only.request";

View File

@@ -1,4 +1,4 @@
import { OrganizationUserType } from "../../../admin-console/enums/organization-user-type";
import { OrganizationUserType } from "../../../admin-console/enums";
import { PermissionsApi } from "../../../admin-console/models/api/permissions.api";
import { SelectionReadOnlyRequest } from "../../../admin-console/models/request/selection-read-only.request";

View File

@@ -1,8 +1,7 @@
import { OrganizationUserStatusType } from "../../../admin-console/enums/organization-user-status-type";
import { OrganizationUserType } from "../../../admin-console/enums/organization-user-type";
import { OrganizationUserStatusType, OrganizationUserType } from "../../../admin-console/enums";
import { PermissionsApi } from "../../../admin-console/models/api/permissions.api";
import { SelectionReadOnlyResponse } from "../../../admin-console/models/response/selection-read-only.response";
import { KdfType } from "../../../enums/kdfType";
import { KdfType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
export class OrganizationUserResponse extends BaseResponse {
@@ -64,7 +63,7 @@ export class OrganizationUserDetailsResponse extends OrganizationUserResponse {
}
}
export class OrganizationUserResetPasswordDetailsReponse extends BaseResponse {
export class OrganizationUserResetPasswordDetailsResponse extends BaseResponse {
kdf: KdfType;
kdfIterations: number;
kdfMemory?: number;

View File

@@ -1,5 +1,4 @@
import { ClientType } from "../enums/clientType";
import { DeviceType } from "../enums/deviceType";
import { ClientType, DeviceType } from "../enums";
interface ToastOptions {
timeout?: number;
@@ -28,15 +27,6 @@ export abstract class PlatformUtilsService {
text: string | string[],
options?: ToastOptions
) => void;
showDialog: (
body: string,
title?: string,
confirmText?: string,
cancelText?: string,
type?: string,
bodyIsHtml?: boolean,
target?: string
) => Promise<boolean>;
isDev: () => boolean;
isSelfHost: () => boolean;
copyToClipboard: (text: string, options?: any) => void | boolean;

View File

@@ -5,7 +5,7 @@ export abstract class SearchService {
indexedEntityId?: string = null;
clearIndex: () => void;
isSearchable: (query: string) => boolean;
indexCiphers: (indexedEntityGuid?: string, ciphersToIndex?: CipherView[]) => Promise<void>;
indexCiphers: (ciphersToIndex: CipherView[], indexedEntityGuid?: string) => void;
searchCiphers: (
query: string,
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],

View File

@@ -4,8 +4,11 @@ import { AccountSettingsSettings } from "../models/domain/account";
export abstract class SettingsService {
settings$: Observable<AccountSettingsSettings>;
disableFavicon$: Observable<boolean>;
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
getEquivalentDomains: (url: string) => string[];
getEquivalentDomains: (url: string) => Set<string>;
setDisableFavicon: (value: boolean) => Promise<any>;
getDisableFavicon: () => boolean;
clear: (userId?: string) => Promise<void>;
}

View File

@@ -8,10 +8,10 @@ import { ProviderData } from "../admin-console/models/data/provider.data";
import { Policy } from "../admin-console/models/domain/policy";
import { CollectionView } from "../admin-console/models/view/collection.view";
import { EnvironmentUrls } from "../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../auth/models/domain/force-reset-password-reason";
import { KdfConfig } from "../auth/models/domain/kdf-config";
import { KdfType } from "../enums/kdfType";
import { ThemeType } from "../enums/themeType";
import { UriMatchType } from "../enums/uriMatchType";
import { BiometricKey } from "../auth/types/biometric-key";
import { KdfType, ThemeType, UriMatchType } from "../enums";
import { EventData } from "../models/data/event.data";
import { ServerConfigData } from "../models/data/server-config.data";
import { Account, AccountSettingsSettings } from "../models/domain/account";
@@ -79,7 +79,7 @@ export abstract class StateService<T extends Account = Account> {
setCryptoMasterKeyB64: (value: string, options?: StorageOptions) => Promise<void>;
getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<string>;
hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise<boolean>;
setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise<void>;
setCryptoMasterKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
getDecryptedCiphers: (options?: StorageOptions) => Promise<CipherView[]>;
setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise<void>;
getDecryptedCollections: (options?: StorageOptions) => Promise<CollectionView[]>;
@@ -145,7 +145,13 @@ export abstract class StateService<T extends Account = Account> {
) => Promise<void>;
getDisableContextMenuItem: (options?: StorageOptions) => Promise<boolean>;
setDisableContextMenuItem: (value: boolean, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this, use SettingsService
*/
getDisableFavicon: (options?: StorageOptions) => Promise<boolean>;
/**
* @deprecated Do not call this, use SettingsService
*/
setDisableFavicon: (value: boolean, options?: StorageOptions) => Promise<void>;
getDisableGa: (options?: StorageOptions) => Promise<boolean>;
setDisableGa: (value: boolean, options?: StorageOptions) => Promise<void>;
@@ -165,8 +171,6 @@ export abstract class StateService<T extends Account = Account> {
setEnableAlwaysOnTop: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableAutoFillOnPageLoad: (options?: StorageOptions) => Promise<boolean>;
setEnableAutoFillOnPageLoad: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableBiometric: (options?: StorageOptions) => Promise<boolean>;
setEnableBiometric: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableBrowserIntegration: (options?: StorageOptions) => Promise<boolean>;
setEnableBrowserIntegration: (value: boolean, options?: StorageOptions) => Promise<void>;
getEnableBrowserIntegrationFingerprint: (options?: StorageOptions) => Promise<boolean>;
@@ -263,8 +267,11 @@ export abstract class StateService<T extends Account = Account> {
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
getForcePasswordReset: (options?: StorageOptions) => Promise<boolean>;
setForcePasswordReset: (value: boolean, options?: StorageOptions) => Promise<void>;
getForcePasswordResetReason: (options?: StorageOptions) => Promise<ForceResetPasswordReason>;
setForcePasswordResetReason: (
value: ForceResetPasswordReason,
options?: StorageOptions
) => Promise<void>;
getInstalledVersion: (options?: StorageOptions) => Promise<string>;
setInstalledVersion: (value: string, options?: StorageOptions) => Promise<void>;
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
@@ -291,14 +298,14 @@ export abstract class StateService<T extends Account = Account> {
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: any }>;
setNeverDomains: (value: { [id: string]: any }, options?: StorageOptions) => Promise<void>;
getNoAutoPromptBiometrics: (options?: StorageOptions) => Promise<boolean>;
setNoAutoPromptBiometrics: (value: boolean, options?: StorageOptions) => Promise<void>;
getNoAutoPromptBiometricsText: (options?: StorageOptions) => Promise<string>;
setNoAutoPromptBiometricsText: (value: string, options?: StorageOptions) => Promise<void>;
getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>;
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise<void>;
getEmergencyAccessInvitation: (options?: StorageOptions) => Promise<any>;
setEmergencyAccessInvitation: (value: any, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use OrganizationService
*/
@@ -348,7 +355,7 @@ export abstract class StateService<T extends Account = Account> {
setTwoFactorToken: (value: string, options?: StorageOptions) => Promise<void>;
getUserId: (options?: StorageOptions) => Promise<string>;
getUsesKeyConnector: (options?: StorageOptions) => Promise<boolean>;
setUsesKeyConnector: (vaule: boolean, options?: StorageOptions) => Promise<void>;
setUsesKeyConnector: (value: boolean, options?: StorageOptions) => Promise<void>;
getVaultTimeout: (options?: StorageOptions) => Promise<number>;
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;

View File

@@ -1,6 +1,12 @@
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
export abstract class VaultTimeoutSettingsService {
setVaultTimeoutOptions: (vaultTimeout: number, vaultTimeoutAction: string) => Promise<void>;
setVaultTimeoutOptions: (
vaultTimeout: number,
vaultTimeoutAction: VaultTimeoutAction
) => Promise<void>;
getVaultTimeout: (userId?: string) => Promise<number>;
getVaultTimeoutAction: (userId?: string) => Promise<VaultTimeoutAction>;
isPinLockSet: () => Promise<[boolean, boolean]>;
isBiometricLockSet: () => Promise<boolean>;
clear: (userId?: string) => Promise<void>;

View File

@@ -15,7 +15,7 @@ import { SeatRequest } from "../../../models/request/seat.request";
import { StorageRequest } from "../../../models/request/storage.request";
import { VerifyBankRequest } from "../../../models/request/verify-bank.request";
import { ListResponse } from "../../../models/response/list.response";
import { OrganizationApiKeyType } from "../../enums/organization-api-key-type";
import { OrganizationApiKeyType } from "../../enums";
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";

View File

@@ -56,13 +56,22 @@ export function canAccessAdmin(i18nService: I18nService) {
);
}
export function isNotProviderUser(org: Organization): boolean {
return !org.isProviderUser;
/**
* Returns `true` if a user is a member of an organization (rather than only being a ProviderUser)
* @deprecated Use organizationService.memberOrganizations$ instead
*/
export function isMember(org: Organization): boolean {
return org.isMember;
}
export abstract class OrganizationService {
organizations$: Observable<Organization[]>;
/**
* Organizations that the user is a member of (excludes organizations that they only have access to via a provider)
*/
memberOrganizations$: Observable<Organization[]>;
get$: (id: string) => Observable<Organization | undefined>;
get: (id: string) => Organization;
getByIdentifier: (identifier: string) => Organization;

View File

@@ -1,5 +1,5 @@
import { ListResponse } from "../../../models/response/list.response";
import { PolicyType } from "../../enums/policy-type";
import { PolicyType } from "../../enums";
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
import { PolicyRequest } from "../../models/request/policy.request";
import { PolicyResponse } from "../../models/response/policy.response";
@@ -7,6 +7,7 @@ import { PolicyResponse } from "../../models/response/policy.response";
export class PolicyApiServiceAbstraction {
getPolicy: (organizationId: string, type: PolicyType) => Promise<PolicyResponse>;
getPolicies: (organizationId: string) => Promise<ListResponse<PolicyResponse>>;
getPoliciesByToken: (
organizationId: string,
token: string,

View File

@@ -1,7 +1,7 @@
import { Observable } from "rxjs";
import { ListResponse } from "../../../models/response/list.response";
import { PolicyType } from "../../enums/policy-type";
import { PolicyType } from "../../enums";
import { PolicyData } from "../../models/data/policy.data";
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
import { Policy } from "../../models/domain/policy";
@@ -10,6 +10,7 @@ import { PolicyResponse } from "../../models/response/policy.response";
export abstract class PolicyService {
policies$: Observable<Policy[]>;
get$: (policyType: PolicyType, policyFilter?: (policy: Policy) => boolean) => Observable<Policy>;
masterPasswordPolicyOptions$: (policies?: Policy[]) => Observable<MasterPasswordPolicyOptions>;
policyAppliesToActiveUser$: (
policyType: PolicyType,

View File

@@ -0,0 +1,8 @@
export * from "./organization-api-key-type.enum";
export * from "./organization-connection-type.enum";
export * from "./organization-user-status-type.enum";
export * from "./organization-user-type.enum";
export * from "./policy-type.enum";
export * from "./provider-user-status-type.enum";
export * from "./provider-user-type.enum";
export * from "./scim-provider-type.enum";

View File

@@ -1,5 +1,5 @@
import { BaseResponse } from "../../../models/response/base.response";
import { ScimProviderType } from "../../enums/scim-provider-type";
import { ScimProviderType } from "../../enums";
export class ScimConfigApi extends BaseResponse {
enabled: boolean;

View File

@@ -1,6 +1,5 @@
import { ProductType } from "../../../enums/productType";
import { OrganizationUserStatusType } from "../../enums/organization-user-status-type";
import { OrganizationUserType } from "../../enums/organization-user-type";
import { ProductType, ProviderType } from "../../../enums";
import { OrganizationUserStatusType, OrganizationUserType } from "../../enums";
import { PermissionsApi } from "../api/permissions.api";
import { ProfileOrganizationResponse } from "../response/profile-organization.response";
@@ -37,7 +36,9 @@ export class OrganizationData {
hasPublicAndPrivateKeys: boolean;
providerId: string;
providerName: string;
providerType?: ProviderType;
isProviderUser: boolean;
isMember: boolean;
familySponsorshipFriendlyName: string;
familySponsorshipAvailable: boolean;
planProductType: ProductType;
@@ -48,7 +49,13 @@ export class OrganizationData {
familySponsorshipToDelete?: boolean;
accessSecretsManager: boolean;
constructor(response: ProfileOrganizationResponse) {
constructor(
response: ProfileOrganizationResponse,
options: {
isMember: boolean;
isProviderUser: boolean;
}
) {
this.id = response.id;
this.name = response.name;
this.status = response.status;
@@ -81,6 +88,7 @@ export class OrganizationData {
this.hasPublicAndPrivateKeys = response.hasPublicAndPrivateKeys;
this.providerId = response.providerId;
this.providerName = response.providerName;
this.providerType = response.providerType;
this.familySponsorshipFriendlyName = response.familySponsorshipFriendlyName;
this.familySponsorshipAvailable = response.familySponsorshipAvailable;
this.planProductType = response.planProductType;
@@ -90,5 +98,8 @@ export class OrganizationData {
this.familySponsorshipValidUntil = response.familySponsorshipValidUntil;
this.familySponsorshipToDelete = response.familySponsorshipToDelete;
this.accessSecretsManager = response.accessSecretsManager;
this.isMember = options.isMember;
this.isProviderUser = options.isProviderUser;
}
}

View File

@@ -1,4 +1,4 @@
import { PolicyType } from "../../enums/policy-type";
import { PolicyType } from "../../enums";
import { PolicyResponse } from "../response/policy.response";
export class PolicyData {

View File

@@ -1,5 +1,4 @@
import { ProviderUserStatusType } from "../../enums/provider-user-status-type";
import { ProviderUserType } from "../../enums/provider-user-type";
import { ProviderUserStatusType, ProviderUserType } from "../../enums";
import { ProfileProviderResponse } from "../response/profile-provider.response";
export class ProviderData {

View File

@@ -1,3 +1,4 @@
import { MasterPasswordPolicyResponse } from "../../../auth/models/response/master-password-policy.response";
import Domain from "../../../models/domain/domain-base";
export class MasterPasswordPolicyOptions extends Domain {
@@ -7,4 +8,26 @@ export class MasterPasswordPolicyOptions extends Domain {
requireLower = false;
requireNumbers = false;
requireSpecial = false;
/**
* Flag to indicate if the policy should be enforced on login.
* If true, and the user's password does not meet the policy requirements,
* the user will be forced to update their password.
*/
enforceOnLogin = false;
static fromResponse(policy: MasterPasswordPolicyResponse): MasterPasswordPolicyOptions {
if (policy == null) {
return null;
}
const options = new MasterPasswordPolicyOptions();
options.minComplexity = policy.minComplexity;
options.minLength = policy.minLength;
options.requireUpper = policy.requireUpper;
options.requireLower = policy.requireLower;
options.requireNumbers = policy.requireNumbers;
options.requireSpecial = policy.requireSpecial;
options.enforceOnLogin = policy.enforceOnLogin;
return options;
}
}

View File

@@ -1,8 +1,7 @@
import { Jsonify } from "type-fest";
import { ProductType } from "../../../enums/productType";
import { OrganizationUserStatusType } from "../../enums/organization-user-status-type";
import { OrganizationUserType } from "../../enums/organization-user-type";
import { ProductType, ProviderType } from "../../../enums";
import { OrganizationUserStatusType, OrganizationUserType } from "../../enums";
import { PermissionsApi } from "../api/permissions.api";
import { OrganizationData } from "../data/organization.data";
@@ -10,7 +9,14 @@ export class Organization {
id: string;
name: string;
status: OrganizationUserStatusType;
/**
* The member's role in the organization.
* Avoid using this for permission checks - use the getters instead (e.g. isOwner, isAdmin, canManageX), because they
* properly handle permission inheritance and relationships.
*/
type: OrganizationUserType;
enabled: boolean;
usePolicies: boolean;
useGroups: boolean;
@@ -39,7 +45,15 @@ export class Organization {
hasPublicAndPrivateKeys: boolean;
providerId: string;
providerName: string;
providerType?: ProviderType;
/**
* Indicates that a user is a ProviderUser for the organization
*/
isProviderUser: boolean;
/**
* Indicates that a user is a member for the organization (may be `false` if they have access via a Provider only)
*/
isMember: boolean;
familySponsorshipFriendlyName: string;
familySponsorshipAvailable: boolean;
planProductType: ProductType;
@@ -87,7 +101,9 @@ export class Organization {
this.hasPublicAndPrivateKeys = obj.hasPublicAndPrivateKeys;
this.providerId = obj.providerId;
this.providerName = obj.providerName;
this.providerType = obj.providerType;
this.isProviderUser = obj.isProviderUser;
this.isMember = obj.isMember;
this.familySponsorshipFriendlyName = obj.familySponsorshipFriendlyName;
this.familySponsorshipAvailable = obj.familySponsorshipAvailable;
this.planProductType = obj.planProductType;
@@ -100,24 +116,29 @@ export class Organization {
}
get canAccess() {
if (this.type === OrganizationUserType.Owner) {
if (this.isOwner) {
return true;
}
return this.enabled && this.status === OrganizationUserStatusType.Confirmed;
}
/**
* Whether a user has Manager permissions or greater
*/
get isManager() {
return (
this.type === OrganizationUserType.Manager ||
this.type === OrganizationUserType.Owner ||
this.type === OrganizationUserType.Admin
);
return this.type === OrganizationUserType.Manager || this.isAdmin;
}
/**
* Whether a user has Admin permissions or greater
*/
get isAdmin() {
return this.type === OrganizationUserType.Owner || this.type === OrganizationUserType.Admin;
return this.type === OrganizationUserType.Admin || this.isOwner;
}
/**
* Whether a user has Owner permissions (including ProviderUsers)
*/
get isOwner() {
return this.type === OrganizationUserType.Owner || this.isProviderUser;
}
@@ -198,8 +219,26 @@ export class Organization {
return this.canManagePolicies;
}
get canManageBilling() {
return this.isOwner && (this.isProviderUser || !this.hasProvider);
get canViewSubscription() {
if (this.canEditSubscription) {
return true;
}
return this.hasProvider && this.providerType === ProviderType.Msp
? this.isProviderUser
: this.isOwner;
}
get canEditSubscription() {
return this.hasProvider ? this.isProviderUser : this.isOwner;
}
get canEditPaymentMethods() {
return this.canEditSubscription;
}
get canViewBillingHistory() {
return this.canEditSubscription;
}
get hasProvider() {

View File

@@ -1,5 +1,5 @@
import Domain from "../../../models/domain/domain-base";
import { PolicyType } from "../../enums/policy-type";
import { PolicyType } from "../../enums";
import { PolicyData } from "../data/policy.data";
export class Policy extends Domain {

View File

@@ -0,0 +1,14 @@
import { Collection } from "../domain/collection";
import { CollectionRequest } from "../request/collection.request";
export class CollectionWithIdRequest extends CollectionRequest {
id: string;
constructor(collection?: Collection) {
if (collection == null) {
return;
}
super(collection);
this.id = collection.id;
}
}

View File

@@ -1,5 +1,5 @@
import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request";
import { OrganizationApiKeyType } from "../../enums/organization-api-key-type";
import { OrganizationApiKeyType } from "../../enums";
export class OrganizationApiKeyRequest extends SecretVerificationRequest {
type: OrganizationApiKeyType = OrganizationApiKeyType.Default;

View File

@@ -1,5 +1,5 @@
import { BillingSyncConfigRequest } from "../../../billing/models/request/billing-sync-config.request";
import { OrganizationConnectionType } from "../../enums/organization-connection-type";
import { OrganizationConnectionType } from "../../enums";
import { ScimConfigRequest } from "./scim-config.request";

View File

@@ -1,5 +1,4 @@
import { PaymentMethodType } from "../../../billing/enums/payment-method-type";
import { PlanType } from "../../../billing/enums/plan-type";
import { PaymentMethodType, PlanType } from "../../../billing/enums";
import { OrganizationKeysRequest } from "./organization-keys.request";

View File

@@ -1,4 +1,4 @@
import { PlanType } from "../../../billing/enums/plan-type";
import { PlanType } from "../../../billing/enums";
import { OrganizationKeysRequest } from "./organization-keys.request";

View File

@@ -1,4 +1,4 @@
import { PlanSponsorshipType } from "../../../../billing/enums/plan-sponsorship-type";
import { PlanSponsorshipType } from "../../../../billing/enums";
export class OrganizationSponsorshipCreateRequest {
sponsoredEmail: string;

View File

@@ -1,4 +1,4 @@
import { PlanSponsorshipType } from "../../../../billing/enums/plan-sponsorship-type";
import { PlanSponsorshipType } from "../../../../billing/enums";
export class OrganizationSponsorshipRedeemRequest {
planSponsorshipType: PlanSponsorshipType;

View File

@@ -1,4 +1,4 @@
import { PolicyType } from "../../enums/policy-type";
import { PolicyType } from "../../enums";
export class PolicyRequest {
type: PolicyType;

View File

@@ -1,4 +1,4 @@
import { ProviderUserType } from "../../../enums/provider-user-type";
import { ProviderUserType } from "../../../enums";
export class ProviderUserInviteRequest {
emails: string[] = [];

View File

@@ -1,4 +1,4 @@
import { ProviderUserType } from "../../../enums/provider-user-type";
import { ProviderUserType } from "../../../enums";
export class ProviderUserUpdateRequest {
type: ProviderUserType;

View File

@@ -1,4 +1,4 @@
import { ScimProviderType } from "../../enums/scim-provider-type";
import { ScimProviderType } from "../../enums";
export class ScimConfigRequest {
constructor(private enabled: boolean, private scimProvider: ScimProviderType = null) {}

View File

@@ -1,5 +1,5 @@
import { BaseResponse } from "../../../models/response/base.response";
import { OrganizationApiKeyType } from "../../enums/organization-api-key-type";
import { OrganizationApiKeyType } from "../../enums";
export class OrganizationApiKeyInformationResponse extends BaseResponse {
keyType: OrganizationApiKeyType;

View File

@@ -1,6 +1,6 @@
import { BillingSyncConfigApi } from "../../../billing/models/api/billing-sync-config.api";
import { BaseResponse } from "../../../models/response/base.response";
import { OrganizationConnectionType } from "../../enums/organization-connection-type";
import { OrganizationConnectionType } from "../../enums";
import { ScimConfigApi } from "../api/scim-config.api";
/**API response config types for OrganizationConnectionResponse */

View File

@@ -1,4 +1,4 @@
import { PlanType } from "../../../billing/enums/plan-type";
import { PlanType } from "../../../billing/enums";
import { PlanResponse } from "../../../billing/models/response/plan.response";
import { BaseResponse } from "../../../models/response/base.response";

View File

@@ -1,5 +1,5 @@
import { BaseResponse } from "../../../models/response/base.response";
import { PolicyType } from "../../enums/policy-type";
import { PolicyType } from "../../enums";
export class PolicyResponse extends BaseResponse {
id: string;

View File

@@ -1,7 +1,6 @@
import { ProductType } from "../../../enums/productType";
import { ProductType, ProviderType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
import { OrganizationUserStatusType } from "../../enums/organization-user-status-type";
import { OrganizationUserType } from "../../enums/organization-user-type";
import { OrganizationUserStatusType, OrganizationUserType } from "../../enums";
import { PermissionsApi } from "../api/permissions.api";
export class ProfileOrganizationResponse extends BaseResponse {
@@ -38,6 +37,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
userId: string;
providerId: string;
providerName: string;
providerType?: ProviderType;
familySponsorshipFriendlyName: string;
familySponsorshipAvailable: boolean;
planProductType: ProductType;
@@ -83,6 +83,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
this.userId = this.getResponseProperty("UserId");
this.providerId = this.getResponseProperty("ProviderId");
this.providerName = this.getResponseProperty("ProviderName");
this.providerType = this.getResponseProperty("ProviderType");
this.familySponsorshipFriendlyName = this.getResponseProperty("FamilySponsorshipFriendlyName");
this.familySponsorshipAvailable = this.getResponseProperty("FamilySponsorshipAvailable");
this.planProductType = this.getResponseProperty("PlanProductType");

View File

@@ -1,6 +1,5 @@
import { BaseResponse } from "../../../models/response/base.response";
import { ProviderUserStatusType } from "../../enums/provider-user-status-type";
import { ProviderUserType } from "../../enums/provider-user-type";
import { ProviderUserStatusType, ProviderUserType } from "../../enums";
import { PermissionsApi } from "../api/permissions.api";
export class ProfileProviderResponse extends BaseResponse {

View File

@@ -1,6 +1,5 @@
import { BaseResponse } from "../../../../models/response/base.response";
import { ProviderUserStatusType } from "../../../enums/provider-user-status-type";
import { ProviderUserType } from "../../../enums/provider-user-type";
import { ProviderUserStatusType, ProviderUserType } from "../../../enums";
import { PermissionsApi } from "../../api/permissions.api";
export class ProviderUserResponse extends BaseResponse {

View File

@@ -3,6 +3,8 @@ import { View } from "../../../models/view/view";
import { Collection } from "../domain/collection";
import { CollectionAccessDetailsResponse } from "../response/collection.response";
export const NestingDelimiter = "/";
export class CollectionView implements View, ITreeNodeObject {
id: string = null;
organizationId: string = null;

View File

@@ -18,7 +18,7 @@ import { VerifyBankRequest } from "../../../models/request/verify-bank.request";
import { ListResponse } from "../../../models/response/list.response";
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction";
import { OrganizationApiKeyType } from "../../enums/organization-api-key-type";
import { OrganizationApiKeyType } from "../../enums";
import { OrganizationCreateRequest } from "../../models/request/organization-create.request";
import { OrganizationKeysRequest } from "../../models/request/organization-keys.request";
import { OrganizationUpdateRequest } from "../../models/request/organization-update.request";

View File

@@ -1,7 +1,10 @@
import { BehaviorSubject, concatMap, map, Observable } from "rxjs";
import { StateService } from "../../../abstractions/state.service";
import { InternalOrganizationService as InternalOrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction";
import {
InternalOrganizationService as InternalOrganizationServiceAbstraction,
isMember,
} from "../../abstractions/organization/organization.service.abstraction";
import { OrganizationData } from "../../models/data/organization.data";
import { Organization } from "../../models/domain/organization";
@@ -9,6 +12,7 @@ export class OrganizationService implements InternalOrganizationServiceAbstracti
protected _organizations = new BehaviorSubject<Organization[]>([]);
organizations$ = this._organizations.asObservable();
memberOrganizations$ = this.organizations$.pipe(map((orgs) => orgs.filter(isMember)));
constructor(private stateService: StateService) {
this.stateService.activeAccountUnlocked$

View File

@@ -6,7 +6,7 @@ import { Utils } from "../../../misc/utils";
import { ListResponse } from "../../../models/response/list.response";
import { PolicyApiServiceAbstraction } from "../../abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "../../abstractions/policy/policy.service.abstraction";
import { PolicyType } from "../../enums/policy-type";
import { PolicyType } from "../../enums";
import { PolicyData } from "../../models/data/policy.data";
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
import { PolicyRequest } from "../../models/request/policy.request";

View File

@@ -1,13 +1,11 @@
import { of, concatMap, BehaviorSubject, Observable, map } from "rxjs";
import { BehaviorSubject, concatMap, map, Observable, of } from "rxjs";
import { StateService } from "../../../abstractions/state.service";
import { Utils } from "../../../misc/utils";
import { ListResponse } from "../../../models/response/list.response";
import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction";
import { InternalPolicyService as InternalPolicyServiceAbstraction } from "../../abstractions/policy/policy.service.abstraction";
import { OrganizationUserStatusType } from "../../enums/organization-user-status-type";
import { OrganizationUserType } from "../../enums/organization-user-type";
import { PolicyType } from "../../enums/policy-type";
import { OrganizationUserStatusType, OrganizationUserType, PolicyType } from "../../enums";
import { PolicyData } from "../../models/data/policy.data";
import { MasterPasswordPolicyOptions } from "../../models/domain/master-password-policy-options";
import { Organization } from "../../models/domain/organization";
@@ -44,6 +42,28 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
.subscribe();
}
/**
* Returns the first policy found that applies to the active user
* @param policyType Policy type to search for
* @param policyFilter Additional filter to apply to the policy
*/
get$(policyType: PolicyType, policyFilter?: (policy: Policy) => boolean): Observable<Policy> {
return this.policies$.pipe(
concatMap(async (policies) => {
const userId = await this.stateService.getUserId();
const appliesToCurrentUser = await this.checkPoliciesThatApplyToUser(
policies,
policyType,
policyFilter,
userId
);
if (appliesToCurrentUser) {
return policies.find((policy) => policy.type === policyType && policy.enabled);
}
})
);
}
/**
* @deprecated Do not call this, use the policies$ observable collection
*/
@@ -117,6 +137,10 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
if (currentPolicy.data.requireSpecial) {
enforcedOptions.requireSpecial = true;
}
if (currentPolicy.data.enforceOnLogin) {
enforcedOptions.enforceOnLogin = true;
}
});
return enforcedOptions;
@@ -240,7 +264,7 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
await this.stateService.setEncryptedPolicies(null, { userId: userId });
}
private isExcemptFromPolicies(organization: Organization, policyType: PolicyType) {
private isExemptFromPolicies(organization: Organization, policyType: PolicyType) {
if (policyType === PolicyType.MaximumVaultTimeout) {
return organization.type === OrganizationUserType.Owner;
}
@@ -271,7 +295,7 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
o.status >= OrganizationUserStatusType.Accepted &&
o.usePolicies &&
policySet.has(o.id) &&
!this.isExcemptFromPolicies(o, policyType)
!this.isExemptFromPolicies(o, policyType)
);
}
}

View File

@@ -18,6 +18,7 @@ export abstract class AuthService {
email: string;
accessCode: string;
authRequestId: string;
ssoEmail2FaSessionToken: string;
logIn: (
credentials:
@@ -37,11 +38,11 @@ export abstract class AuthService {
authingWithPassword: () => boolean;
authingWithPasswordless: () => boolean;
getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>;
authResponsePushNotifiction: (notification: AuthRequestPushNotification) => Promise<any>;
authResponsePushNotification: (notification: AuthRequestPushNotification) => Promise<any>;
passwordlessLogin: (
id: string,
key: string,
requestApproved: boolean
) => Promise<AuthRequestResponse>;
getPushNotifcationObs$: () => Observable<any>;
getPushNotificationObs$: () => Observable<any>;
}

View File

@@ -7,20 +7,24 @@ import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { Utils } from "../../misc/utils";
import { Account, AccountProfile, AccountTokens } from "../../models/domain/account";
import { EncString } from "../../models/domain/enc-string";
import { PasswordGenerationService } from "../../tools/generator/password";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { AuthResult } from "../models/domain/auth-result";
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
import { PasswordLogInCredentials } from "../models/domain/log-in-credentials";
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
import { IdentityCaptchaResponse } from "../models/response/identity-captcha.response";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
import { PasswordLogInStrategy } from "./password-login.strategy";
@@ -50,7 +54,9 @@ const twoFactorProviderType = TwoFactorProviderType.Authenticator;
const twoFactorToken = "TWO_FACTOR_TOKEN";
const twoFactorRemember = true;
export function identityTokenResponseFactory() {
export function identityTokenResponseFactory(
masterPasswordPolicyResponse: MasterPasswordPolicyResponse = null
) {
return new IdentityTokenResponse({
ForcePasswordReset: false,
Kdf: kdf,
@@ -63,6 +69,7 @@ export function identityTokenResponseFactory() {
refresh_token: refreshToken,
scope: "api offline_access",
token_type: "Bearer",
MasterPasswordPolicy: masterPasswordPolicyResponse,
});
}
@@ -77,6 +84,8 @@ describe("LogInStrategy", () => {
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let authService: MockProxy<AuthService>;
let policyService: MockProxy<PolicyService>;
let passwordGenerationService: MockProxy<PasswordGenerationService>;
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
@@ -92,6 +101,8 @@ describe("LogInStrategy", () => {
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
authService = mock<AuthService>();
policyService = mock<PolicyService>();
passwordGenerationService = mock<PasswordGenerationService>();
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.calledWith(accessToken).mockResolvedValue(decodedToken);
@@ -107,6 +118,8 @@ describe("LogInStrategy", () => {
logService,
stateService,
twoFactorService,
passwordGenerationService,
policyService,
authService
);
credentials = new PasswordLogInCredentials(email, masterPassword);
@@ -155,7 +168,7 @@ describe("LogInStrategy", () => {
const result = await passwordLogInStrategy.logIn(credentials);
expect(result).toEqual({
forcePasswordReset: true,
forcePasswordReset: ForceResetPasswordReason.AdminForcePasswordReset,
resetMasterPassword: true,
twoFactorProviders: null,
captchaSiteKey: "",
@@ -210,6 +223,9 @@ describe("LogInStrategy", () => {
TwoFactorProviders2: { 0: null },
error: "invalid_grant",
error_description: "Two factor required.",
// only sent for emailed 2FA
email: undefined,
ssoEmail2faSessionToken: undefined,
});
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
@@ -225,6 +241,39 @@ describe("LogInStrategy", () => {
expect(result).toEqual(expected);
});
it("rejects login if 2FA via email is required + maps required information", async () => {
// Sample response where Email 2FA required
const userEmail = "kyle@bitwarden.com";
const ssoEmail2FaSessionToken =
"BwSsoEmail2FaSessionToken_CfDJ8AMrVzKqBFpKqzzsahUx8ubIi9AhHm6aLHDLpCUYc3QV3qC14iuSVkNg57Q7-kGQUn1z87bGY1WP58jFMNJ6ndaurIgQWNfPNN4DG-dBhvzarOAZ0RKY5oKT5futWm6_k9NMMGd8PcGGHg5Pq1_koOIwRtiXO3IpD-bemB7m8oEvbj__JTQP3Mcz-UediFlCbYBKU3wyIiBL_tF8hW5D4RAUa5ZzXIuauJiiCdDS7QOzBcqcusVAPGFfKjfIdAwFfKSOYd5KmYrhK7Y7ymjweP_igPYKB5aMfcVaYr5ux-fdffeJTGqtJorwNjLUYNv7KA";
const tokenResponse = new IdentityTwoFactorResponse({
TwoFactorProviders: ["1"],
TwoFactorProviders2: { "1": { Email: "k***@bitwarden.com" } },
error: "invalid_grant",
error_description: "Two factor required.",
// only sent for emailed 2FA
email: userEmail,
ssoEmail2faSessionToken: ssoEmail2FaSessionToken,
});
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
const result = await passwordLogInStrategy.logIn(credentials);
expect(stateService.addAccount).not.toHaveBeenCalled();
expect(messagingService.send).not.toHaveBeenCalled();
const expected = new AuthResult();
expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
expected.twoFactorProviders.set(1, { Email: "k***@bitwarden.com" });
expected.email = userEmail;
expected.ssoEmail2FaSessionToken = ssoEmail2FaSessionToken;
expect(result).toEqual(expected);
});
it("sends stored 2FA token to server", async () => {
tokenService.getTwoFactorToken.mockResolvedValue(twoFactorToken);
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());

View File

@@ -11,11 +11,12 @@ import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { AuthResult } from "../models/domain/auth-result";
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
import {
UserApiLogInCredentials,
PasswordlessLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
PasswordlessLogInCredentials,
UserApiLogInCredentials,
} from "../models/domain/log-in-credentials";
import { DeviceRequest } from "../models/request/identity-token/device.request";
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
@@ -26,6 +27,8 @@ import { IdentityCaptchaResponse } from "../models/response/identity-captcha.res
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse;
export abstract class LogInStrategy {
protected abstract tokenRequest: UserApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
protected captchaBypassToken: string = null;
@@ -58,20 +61,21 @@ export abstract class LogInStrategy {
captchaResponse: string = null
): Promise<AuthResult> {
this.tokenRequest.setTwoFactor(twoFactor);
return this.startLogIn();
const [authResult] = await this.startLogIn();
return authResult;
}
protected async startLogIn(): Promise<AuthResult> {
protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> {
this.twoFactorService.clearSelectedProvider();
const response = await this.apiService.postIdentityToken(this.tokenRequest);
if (response instanceof IdentityTwoFactorResponse) {
return this.processTwoFactorResponse(response);
return [await this.processTwoFactorResponse(response), response];
} else if (response instanceof IdentityCaptchaResponse) {
return this.processCaptchaResponse(response);
return [await this.processCaptchaResponse(response), response];
} else if (response instanceof IdentityTokenResponse) {
return this.processTokenResponse(response);
return [await this.processTokenResponse(response), response];
}
throw new Error("Invalid response object.");
@@ -126,7 +130,10 @@ export abstract class LogInStrategy {
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
const result = new AuthResult();
result.resetMasterPassword = response.resetMasterPassword;
result.forcePasswordReset = response.forcePasswordReset;
if (response.forcePasswordReset) {
result.forcePasswordReset = ForceResetPasswordReason.AdminForcePasswordReset;
}
await this.saveAccountInformation(response);
@@ -153,8 +160,11 @@ export abstract class LogInStrategy {
private async processTwoFactorResponse(response: IdentityTwoFactorResponse): Promise<AuthResult> {
const result = new AuthResult();
result.twoFactorProviders = response.twoFactorProviders2;
this.twoFactorService.setProviders(response);
this.captchaBypassToken = response.captchaToken ?? null;
result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken;
result.email = response.email;
return result;
}

View File

@@ -7,13 +7,19 @@ import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { HashPurpose } from "../../enums/hashPurpose";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { HashPurpose } from "../../enums";
import { Utils } from "../../misc/utils";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { PasswordGenerationService } from "../../tools/generator/password";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
import { PasswordLogInCredentials } from "../models/domain/log-in-credentials";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
import { identityTokenResponseFactory } from "./login.strategy.spec";
import { PasswordLogInStrategy } from "./password-login.strategy";
@@ -28,6 +34,10 @@ const preloginKey = new SymmetricCryptoKey(
)
);
const deviceId = Utils.newGuid();
const masterPasswordPolicy = new MasterPasswordPolicyResponse({
EnforceOnLogin: true,
MinLength: 8,
});
describe("PasswordLogInStrategy", () => {
let cryptoService: MockProxy<CryptoService>;
@@ -40,6 +50,8 @@ describe("PasswordLogInStrategy", () => {
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let authService: MockProxy<AuthService>;
let policyService: MockProxy<PolicyService>;
let passwordGenerationService: MockProxy<PasswordGenerationService>;
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
@@ -55,6 +67,8 @@ describe("PasswordLogInStrategy", () => {
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
authService = mock<AuthService>();
policyService = mock<PolicyService>();
passwordGenerationService = mock<PasswordGenerationService>();
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.mockResolvedValue({});
@@ -68,6 +82,8 @@ describe("PasswordLogInStrategy", () => {
.calledWith(masterPassword, expect.anything(), HashPurpose.LocalAuthorization)
.mockResolvedValue(localHashedPassword);
policyService.evaluateMasterPassword.mockReturnValue(true);
passwordLogInStrategy = new PasswordLogInStrategy(
cryptoService,
apiService,
@@ -78,11 +94,15 @@ describe("PasswordLogInStrategy", () => {
logService,
stateService,
twoFactorService,
passwordGenerationService,
policyService,
authService
);
credentials = new PasswordLogInCredentials(email, masterPassword);
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
apiService.postIdentityToken.mockResolvedValue(
identityTokenResponseFactory(masterPasswordPolicy)
);
});
it("sends master password credentials to the server", async () => {
@@ -110,4 +130,75 @@ describe("PasswordLogInStrategy", () => {
expect(cryptoService.setKey).toHaveBeenCalledWith(preloginKey);
expect(cryptoService.setKeyHash).toHaveBeenCalledWith(localHashedPassword);
});
it("does not force the user to update their master password when there are no requirements", async () => {
apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory(null));
const result = await passwordLogInStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).not.toHaveBeenCalled();
expect(result.forcePasswordReset).toEqual(ForceResetPasswordReason.None);
});
it("does not force the user to update their master password when it meets requirements", async () => {
passwordGenerationService.passwordStrength.mockReturnValue({ score: 5 } as any);
policyService.evaluateMasterPassword.mockReturnValue(true);
const result = await passwordLogInStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
expect(result.forcePasswordReset).toEqual(ForceResetPasswordReason.None);
});
it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => {
passwordGenerationService.passwordStrength.mockReturnValue({ score: 0 } as any);
policyService.evaluateMasterPassword.mockReturnValue(false);
const result = await passwordLogInStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
expect(stateService.setForcePasswordResetReason).toHaveBeenCalledWith(
ForceResetPasswordReason.WeakMasterPassword
);
expect(result.forcePasswordReset).toEqual(ForceResetPasswordReason.WeakMasterPassword);
});
it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => {
passwordGenerationService.passwordStrength.mockReturnValue({ score: 0 } as any);
policyService.evaluateMasterPassword.mockReturnValue(false);
const token2FAResponse = new IdentityTwoFactorResponse({
TwoFactorProviders: ["0"],
TwoFactorProviders2: { 0: null },
error: "invalid_grant",
error_description: "Two factor required.",
MasterPasswordPolicy: masterPasswordPolicy,
});
// First login request fails requiring 2FA
apiService.postIdentityToken.mockResolvedValueOnce(token2FAResponse);
const firstResult = await passwordLogInStrategy.logIn(credentials);
// Second login request succeeds
apiService.postIdentityToken.mockResolvedValueOnce(
identityTokenResponseFactory(masterPasswordPolicy)
);
const secondResult = await passwordLogInStrategy.logInTwoFactor(
{
provider: TwoFactorProviderType.Authenticator,
token: "123456",
remember: false,
},
""
);
// First login attempt should not save the force password reset options
expect(firstResult.forcePasswordReset).toEqual(ForceResetPasswordReason.None);
// Second login attempt should save the force password reset options and return in result
expect(stateService.setForcePasswordResetReason).toHaveBeenCalledWith(
ForceResetPasswordReason.WeakMasterPassword
);
expect(secondResult.forcePasswordReset).toEqual(ForceResetPasswordReason.WeakMasterPassword);
});
});

View File

@@ -5,15 +5,22 @@ import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { HashPurpose } from "../../enums/hashPurpose";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "../../admin-console/models/domain/master-password-policy-options";
import { HashPurpose } from "../../enums";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { PasswordGenerationServiceAbstraction } from "../../tools/generator/password";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { AuthResult } from "../models/domain/auth-result";
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
import { PasswordLogInCredentials } from "../models/domain/log-in-credentials";
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
import { IdentityCaptchaResponse } from "../models/response/identity-captcha.response";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
import { LogInStrategy } from "./login.strategy";
@@ -31,6 +38,12 @@ export class PasswordLogInStrategy extends LogInStrategy {
private localHashedPassword: string;
private key: SymmetricCryptoKey;
/**
* Options to track if the user needs to update their password due to a password that does not meet an organization's
* master password policy.
*/
private forcePasswordResetReason: ForceResetPasswordReason = ForceResetPasswordReason.None;
constructor(
cryptoService: CryptoService,
apiService: ApiService,
@@ -39,8 +52,10 @@ export class PasswordLogInStrategy extends LogInStrategy {
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
stateService: StateService,
protected stateService: StateService,
twoFactorService: TwoFactorService,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private policyService: PolicyService,
private authService: AuthService
) {
super(
@@ -66,7 +81,19 @@ export class PasswordLogInStrategy extends LogInStrategy {
captchaResponse: string
): Promise<AuthResult> {
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
return super.logInTwoFactor(twoFactor);
const result = await super.logInTwoFactor(twoFactor);
// 2FA was successful, save the force update password options with the state service if defined
if (
!result.requiresTwoFactor &&
!result.requiresCaptcha &&
this.forcePasswordResetReason != ForceResetPasswordReason.None
) {
await this.stateService.setForcePasswordResetReason(this.forcePasswordResetReason);
result.forcePasswordReset = this.forcePasswordResetReason;
}
return result;
}
async logIn(credentials: PasswordLogInCredentials) {
@@ -90,6 +117,52 @@ export class PasswordLogInStrategy extends LogInStrategy {
await this.buildDeviceRequest()
);
return this.startLogIn();
const [authResult, identityResponse] = await this.startLogIn();
const masterPasswordPolicyOptions =
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
// The identity result can contain master password policies for the user's organizations
if (masterPasswordPolicyOptions?.enforceOnLogin) {
// If there is a policy active, evaluate the supplied password before its no longer in memory
const meetsRequirements = this.evaluateMasterPassword(
credentials,
masterPasswordPolicyOptions
);
if (!meetsRequirements) {
if (authResult.requiresCaptcha || authResult.requiresTwoFactor) {
// Save the flag to this strategy for later use as the master password is about to pass out of scope
this.forcePasswordResetReason = ForceResetPasswordReason.WeakMasterPassword;
} else {
// Authentication was successful, save the force update password options with the state service
await this.stateService.setForcePasswordResetReason(
ForceResetPasswordReason.WeakMasterPassword
);
authResult.forcePasswordReset = ForceResetPasswordReason.WeakMasterPassword;
}
}
}
return authResult;
}
private getMasterPasswordPolicyOptionsFromResponse(
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse
): MasterPasswordPolicyOptions {
if (response == null || response instanceof IdentityCaptchaResponse) {
return null;
}
return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy);
}
private evaluateMasterPassword(
{ masterPassword, email }: PasswordLogInCredentials,
options: MasterPasswordPolicyOptions
): boolean {
const passwordStrength = this.passwordGenerationService.passwordStrength(
masterPassword,
email
)?.score;
return this.policyService.evaluateMasterPassword(passwordStrength, masterPassword, options);
}
}

View File

@@ -81,6 +81,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
);
this.tokenRequest.setPasswordlessAccessCode(credentials.authRequestId);
return this.startLogIn();
const [authResult] = await this.startLogIn();
return authResult;
}
}

View File

@@ -18,6 +18,12 @@ export class SsoLogInStrategy extends LogInStrategy {
tokenRequest: SsoTokenRequest;
orgId: string;
// A session token server side to serve as an authentication factor for the user
// in order to send email OTPs to the user's configured 2FA email address
// as we don't have a master password hash or other verifiable secret when using SSO.
ssoEmail2FaSessionToken?: string;
email?: string; // email not preserved through SSO process so get from server
constructor(
cryptoService: CryptoService,
apiService: ApiService,
@@ -65,6 +71,11 @@ export class SsoLogInStrategy extends LogInStrategy {
await this.buildDeviceRequest()
);
return this.startLogIn();
const [ssoAuthResult] = await this.startLogIn();
this.email = ssoAuthResult.email;
this.ssoEmail2FaSessionToken = ssoAuthResult.ssoEmail2FaSessionToken;
return ssoAuthResult;
}
}

View File

@@ -59,7 +59,8 @@ export class UserApiLogInStrategy extends LogInStrategy {
await this.buildDeviceRequest()
);
return this.startLogIn();
const [authResult] = await this.startLogIn();
return authResult;
}
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {

View File

@@ -1,11 +1,15 @@
import { Utils } from "../../../misc/utils";
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
import { ForceResetPasswordReason } from "./force-reset-password-reason";
export class AuthResult {
captchaSiteKey = "";
resetMasterPassword = false;
forcePasswordReset = false;
forcePasswordReset: ForceResetPasswordReason = ForceResetPasswordReason.None;
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
ssoEmail2FaSessionToken?: string;
email: string;
get requiresCaptcha() {
return !Utils.isNullOrWhitespace(this.captchaSiteKey);

View File

@@ -0,0 +1,17 @@
export enum ForceResetPasswordReason {
/**
* A password reset should not be forced.
*/
None,
/**
* Occurs when an organization admin forces a user to reset their password.
*/
AdminForcePasswordReset,
/**
* Occurs when a user logs in / unlocks their vault with a master password that does not meet an organization's
* master password policy that is enforced on login/unlock.
*/
WeakMasterPassword,
}

View File

@@ -1,5 +1,5 @@
import { PlatformUtilsService } from "../../../../abstractions/platformUtils.service";
import { DeviceType } from "../../../../enums/deviceType";
import { DeviceType } from "../../../../enums";
export class DeviceRequest {
type: DeviceType;

View File

@@ -1,4 +1,4 @@
import { ClientType } from "../../../../enums/clientType";
import { ClientType } from "../../../../enums";
import { Utils } from "../../../../misc/utils";
import { CaptchaProtectedRequest } from "../captcha-protected.request";

View File

@@ -1,4 +1,4 @@
import { KdfType } from "../../../enums/kdfType";
import { KdfType } from "../../../enums";
import { KeysRequest } from "../../../models/request/keys.request";
import { KdfConfig } from "../domain/kdf-config";

View File

@@ -1,4 +1,4 @@
import { KdfType } from "../../../enums/kdfType";
import { KdfType } from "../../../enums";
import { KeysRequest } from "../../../models/request/keys.request";
export class SetPasswordRequest {

View File

@@ -4,4 +4,5 @@ export class TwoFactorEmailRequest extends SecretVerificationRequest {
email: string;
deviceIdentifier: string;
authRequestId: string;
ssoEmail2FaSessionToken?: string;
}

View File

@@ -1,4 +1,4 @@
import { DeviceType } from "../../../enums/deviceType";
import { DeviceType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
const RequestTimeOut = 60000 * 15; //15 Minutes

View File

@@ -1,4 +1,4 @@
import { DeviceType } from "../../../enums/deviceType";
import { DeviceType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
export class DeviceResponse extends BaseResponse {

View File

@@ -1,4 +1,4 @@
import { KdfType } from "../../../enums/kdfType";
import { KdfType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
import { CipherResponse } from "../../../vault/models/response/cipher.response";
import { EmergencyAccessStatusType } from "../../enums/emergency-access-status-type";

View File

@@ -1,6 +1,8 @@
import { KdfType } from "../../../enums/kdfType";
import { KdfType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
import { MasterPasswordPolicyResponse } from "./master-password-policy.response";
export class IdentityTokenResponse extends BaseResponse {
accessToken: string;
expiresIn: number;
@@ -16,6 +18,7 @@ export class IdentityTokenResponse extends BaseResponse {
kdfMemory?: number;
kdfParallelism?: number;
forcePasswordReset: boolean;
masterPasswordPolicy: MasterPasswordPolicyResponse;
apiUseKeyConnector: boolean;
keyConnectorUrl: string;
@@ -37,5 +40,8 @@ export class IdentityTokenResponse extends BaseResponse {
this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset");
this.apiUseKeyConnector = this.getResponseProperty("ApiUseKeyConnector");
this.keyConnectorUrl = this.getResponseProperty("KeyConnectorUrl");
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
this.getResponseProperty("MasterPasswordPolicy")
);
}
}

View File

@@ -1,10 +1,15 @@
import { BaseResponse } from "../../../models/response/base.response";
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
import { MasterPasswordPolicyResponse } from "./master-password-policy.response";
export class IdentityTwoFactorResponse extends BaseResponse {
twoFactorProviders: TwoFactorProviderType[];
twoFactorProviders2 = new Map<TwoFactorProviderType, { [key: string]: string }>();
captchaToken: string;
ssoEmail2faSessionToken: string;
email?: string;
masterPasswordPolicy?: MasterPasswordPolicyResponse;
constructor(response: any) {
super(response);
@@ -19,5 +24,11 @@ export class IdentityTwoFactorResponse extends BaseResponse {
}
}
}
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
this.getResponseProperty("MasterPasswordPolicy")
);
this.ssoEmail2faSessionToken = this.getResponseProperty("SsoEmail2faSessionToken");
this.email = this.getResponseProperty("Email");
}
}

View File

@@ -0,0 +1,29 @@
import { BaseResponse } from "../../../models/response/base.response";
export class MasterPasswordPolicyResponse extends BaseResponse {
minComplexity: number;
minLength: number;
requireUpper: boolean;
requireLower: boolean;
requireNumbers: boolean;
requireSpecial: boolean;
/**
* Flag to indicate if the policy should be enforced on login.
* If true, and the user's password does not meet the policy requirements,
* the user will be forced to update their password.
*/
enforceOnLogin: boolean;
constructor(response: any) {
super(response);
this.minComplexity = this.getResponseProperty("MinComplexity");
this.minLength = this.getResponseProperty("MinLength");
this.requireUpper = this.getResponseProperty("RequireUpper");
this.requireLower = this.getResponseProperty("RequireLower");
this.requireNumbers = this.getResponseProperty("RequireNumbers");
this.requireSpecial = this.getResponseProperty("RequireSpecial");
this.enforceOnLogin = this.getResponseProperty("EnforceOnLogin");
}
}

View File

@@ -1,4 +1,4 @@
import { KdfType } from "../../../enums/kdfType";
import { KdfType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
export class PreloginResponse extends BaseResponse {

View File

@@ -10,13 +10,14 @@ import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { KdfType } from "../../enums/kdfType";
import { KeySuffixOptions } from "../../enums/keySuffixOptions";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { KdfType, KeySuffixOptions } from "../../enums";
import { Utils } from "../../misc/utils";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { PreloginRequest } from "../../models/request/prelogin.request";
import { ErrorResponse } from "../../models/response/error.response";
import { AuthRequestPushNotification } from "../../models/response/notification.response";
import { PasswordGenerationServiceAbstraction } from "../../tools/generator/password";
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
@@ -30,10 +31,10 @@ import { UserApiLogInStrategy } from "../login-strategies/user-api-login.strateg
import { AuthResult } from "../models/domain/auth-result";
import { KdfConfig } from "../models/domain/kdf-config";
import {
UserApiLogInCredentials,
PasswordlessLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
PasswordlessLogInCredentials,
UserApiLogInCredentials,
} from "../models/domain/log-in-credentials";
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
import { PasswordlessAuthRequest } from "../models/request/passwordless-auth.request";
@@ -45,7 +46,8 @@ export class AuthService implements AuthServiceAbstraction {
get email(): string {
if (
this.logInStrategy instanceof PasswordLogInStrategy ||
this.logInStrategy instanceof PasswordlessLogInStrategy
this.logInStrategy instanceof PasswordlessLogInStrategy ||
this.logInStrategy instanceof SsoLogInStrategy
) {
return this.logInStrategy.email;
}
@@ -71,6 +73,12 @@ export class AuthService implements AuthServiceAbstraction {
: null;
}
get ssoEmail2FaSessionToken(): string {
return this.logInStrategy instanceof SsoLogInStrategy
? this.logInStrategy.ssoEmail2FaSessionToken
: null;
}
private logInStrategy:
| UserApiLogInStrategy
| PasswordLogInStrategy
@@ -93,7 +101,9 @@ export class AuthService implements AuthServiceAbstraction {
protected stateService: StateService,
protected twoFactorService: TwoFactorService,
protected i18nService: I18nService,
protected encryptService: EncryptService
protected encryptService: EncryptService,
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
protected policyService: PolicyService
) {}
async logIn(
@@ -123,6 +133,8 @@ export class AuthService implements AuthServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.passwordGenerationService,
this.policyService,
this
);
break;
@@ -272,11 +284,11 @@ export class AuthService implements AuthServiceAbstraction {
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfConfig);
}
async authResponsePushNotifiction(notification: AuthRequestPushNotification): Promise<any> {
async authResponsePushNotification(notification: AuthRequestPushNotification): Promise<any> {
this.pushNotificationSubject.next(notification.id);
}
getPushNotifcationObs$(): Observable<any> {
getPushNotificationObs$(): Observable<any> {
return this.pushNotificationSubject.asObservable();
}

View File

@@ -4,7 +4,7 @@ import { CryptoFunctionService } from "../../abstractions/cryptoFunction.service
import { LogService } from "../../abstractions/log.service";
import { StateService } from "../../abstractions/state.service";
import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserType } from "../../admin-console/enums/organization-user-type";
import { OrganizationUserType } from "../../admin-console/enums";
import { Utils } from "../../misc/utils";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { KeysRequest } from "../../models/request/keys.request";

View File

@@ -0,0 +1,6 @@
import { CsprngString } from "../../types/csprng";
export type BiometricKey = {
key: string;
clientEncKeyHalf: CsprngString;
};

Some files were not shown because too many files have changed in this diff Show More