1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-05 23:53:21 +00:00

[AC-3047] Refactor LoginCommand to only use organization api key login (#621)

* Add tests

* Remove unused code from LoginCommand and refactor

* Remove unused services

* Remove unused npm deps

* Install missing type-fest dep
This commit is contained in:
Thomas Rittson
2024-09-19 07:50:40 +10:00
committed by GitHub
parent 3d9465917d
commit 9dc497dd13
12 changed files with 217 additions and 1445 deletions

View File

@@ -12,9 +12,7 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@/jslib/c
import { LogService } from "@/jslib/common/src/abstractions/log.service"; import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service"; import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
import { OrganizationService as OrganizationServiceAbstraction } from "@/jslib/common/src/abstractions/organization.service"; import { OrganizationService as OrganizationServiceAbstraction } from "@/jslib/common/src/abstractions/organization.service";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@/jslib/common/src/abstractions/passwordGeneration.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
import { PolicyService as PolicyServiceAbstraction } from "@/jslib/common/src/abstractions/policy.service";
import { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service"; import { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service"; import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service"; import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
@@ -31,8 +29,6 @@ import { CryptoService } from "@/jslib/common/src/services/crypto.service";
import { EnvironmentService } from "@/jslib/common/src/services/environment.service"; import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service"; import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
import { OrganizationService } from "@/jslib/common/src/services/organization.service"; import { OrganizationService } from "@/jslib/common/src/services/organization.service";
import { PasswordGenerationService } from "@/jslib/common/src/services/passwordGeneration.service";
import { PolicyService } from "@/jslib/common/src/services/policy.service";
import { StateService } from "@/jslib/common/src/services/state.service"; import { StateService } from "@/jslib/common/src/services/state.service";
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service"; import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
import { TokenService } from "@/jslib/common/src/services/token.service"; import { TokenService } from "@/jslib/common/src/services/token.service";
@@ -104,11 +100,6 @@ import { ValidationService } from "./validation.service";
StateServiceAbstraction, StateServiceAbstraction,
], ],
}), }),
safeProvider({
provide: PasswordGenerationServiceAbstraction,
useClass: PasswordGenerationService,
deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction],
}),
safeProvider({ safeProvider({
provide: ApiServiceAbstraction, provide: ApiServiceAbstraction,
useFactory: ( useFactory: (
@@ -173,11 +164,6 @@ import { ValidationService } from "./validation.service";
), ),
deps: [StorageServiceAbstraction, SECURE_STORAGE], deps: [StorageServiceAbstraction, SECURE_STORAGE],
}), }),
safeProvider({
provide: PolicyServiceAbstraction,
useClass: PolicyService,
deps: [StateServiceAbstraction, OrganizationServiceAbstraction, ApiServiceAbstraction],
}),
safeProvider({ safeProvider({
provide: KeyConnectorServiceAbstraction, provide: KeyConnectorServiceAbstraction,
useClass: KeyConnectorService, useClass: KeyConnectorService,

View File

@@ -1,20 +0,0 @@
import * as zxcvbn from "zxcvbn";
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions";
export abstract class PasswordGenerationService {
generatePassword: (options: any) => Promise<string>;
generatePassphrase: (options: any) => Promise<string>;
getOptions: () => Promise<[any, PasswordGeneratorPolicyOptions]>;
enforcePasswordGeneratorPoliciesOnOptions: (
options: any,
) => Promise<[any, PasswordGeneratorPolicyOptions]>;
getPasswordGeneratorPolicyOptions: () => Promise<PasswordGeneratorPolicyOptions>;
saveOptions: (options: any) => Promise<any>;
getHistory: () => Promise<GeneratedPasswordHistory[]>;
addHistory: (password: string) => Promise<any>;
clear: (userId?: string) => Promise<any>;
passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult;
normalizeOptions: (options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) => void;
}

View File

@@ -1,32 +0,0 @@
import { PolicyType } from "../enums/policyType";
import { PolicyData } from "../models/data/policyData";
import { MasterPasswordPolicyOptions } from "../models/domain/masterPasswordPolicyOptions";
import { Policy } from "../models/domain/policy";
import { ResetPasswordPolicyOptions } from "../models/domain/resetPasswordPolicyOptions";
import { ListResponse } from "../models/response/listResponse";
import { PolicyResponse } from "../models/response/policyResponse";
export abstract class PolicyService {
clearCache: () => void;
getAll: (type?: PolicyType, userId?: string) => Promise<Policy[]>;
getPolicyForOrganization: (policyType: PolicyType, organizationId: string) => Promise<Policy>;
replace: (policies: { [id: string]: PolicyData }) => Promise<any>;
clear: (userId?: string) => Promise<any>;
getMasterPasswordPoliciesForInvitedUsers: (orgId: string) => Promise<MasterPasswordPolicyOptions>;
getMasterPasswordPolicyOptions: (policies?: Policy[]) => Promise<MasterPasswordPolicyOptions>;
evaluateMasterPassword: (
passwordStrength: number,
newPassword: string,
enforcedPolicyOptions?: MasterPasswordPolicyOptions,
) => boolean;
getResetPasswordPolicyOptions: (
policies: Policy[],
orgId: string,
) => [ResetPasswordPolicyOptions, boolean];
mapPoliciesFromToken: (policiesResponse: ListResponse<PolicyResponse>) => Policy[];
policyAppliesToUser: (
policyType: PolicyType,
policyFilter?: (policy: Policy) => boolean,
userId?: string,
) => Promise<boolean>;
}

View File

@@ -1,572 +0,0 @@
import * as zxcvbn from "zxcvbn";
import { CryptoService } from "../abstractions/crypto.service";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "../abstractions/passwordGeneration.service";
import { PolicyService } from "../abstractions/policy.service";
import { StateService } from "../abstractions/state.service";
import { PolicyType } from "../enums/policyType";
import { EEFLongWordList } from "../misc/wordlist";
import { EncString } from "../models/domain/encString";
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions";
import { Policy } from "../models/domain/policy";
const DefaultOptions = {
length: 14,
ambiguous: false,
number: true,
minNumber: 1,
uppercase: true,
minUppercase: 0,
lowercase: true,
minLowercase: 0,
special: false,
minSpecial: 1,
type: "password",
numWords: 3,
wordSeparator: "-",
capitalize: false,
includeNumber: false,
};
const MaxPasswordsInHistory = 100;
export class PasswordGenerationService implements PasswordGenerationServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private policyService: PolicyService,
private stateService: StateService,
) {}
async generatePassword(options: any): Promise<string> {
// overload defaults with given options
const o = Object.assign({}, DefaultOptions, options);
if (o.type === "passphrase") {
return this.generatePassphrase(options);
}
// sanitize
this.sanitizePasswordLength(o, true);
const minLength: number = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial;
if (o.length < minLength) {
o.length = minLength;
}
const positions: string[] = [];
if (o.lowercase && o.minLowercase > 0) {
for (let i = 0; i < o.minLowercase; i++) {
positions.push("l");
}
}
if (o.uppercase && o.minUppercase > 0) {
for (let i = 0; i < o.minUppercase; i++) {
positions.push("u");
}
}
if (o.number && o.minNumber > 0) {
for (let i = 0; i < o.minNumber; i++) {
positions.push("n");
}
}
if (o.special && o.minSpecial > 0) {
for (let i = 0; i < o.minSpecial; i++) {
positions.push("s");
}
}
while (positions.length < o.length) {
positions.push("a");
}
// shuffle
await this.shuffleArray(positions);
// build out the char sets
let allCharSet = "";
let lowercaseCharSet = "abcdefghijkmnopqrstuvwxyz";
if (o.ambiguous) {
lowercaseCharSet += "l";
}
if (o.lowercase) {
allCharSet += lowercaseCharSet;
}
let uppercaseCharSet = "ABCDEFGHJKLMNPQRSTUVWXYZ";
if (o.ambiguous) {
uppercaseCharSet += "IO";
}
if (o.uppercase) {
allCharSet += uppercaseCharSet;
}
let numberCharSet = "23456789";
if (o.ambiguous) {
numberCharSet += "01";
}
if (o.number) {
allCharSet += numberCharSet;
}
const specialCharSet = "!@#$%^&*";
if (o.special) {
allCharSet += specialCharSet;
}
let password = "";
for (let i = 0; i < o.length; i++) {
let positionChars: string;
switch (positions[i]) {
case "l":
positionChars = lowercaseCharSet;
break;
case "u":
positionChars = uppercaseCharSet;
break;
case "n":
positionChars = numberCharSet;
break;
case "s":
positionChars = specialCharSet;
break;
case "a":
positionChars = allCharSet;
break;
default:
break;
}
const randomCharIndex = await this.cryptoService.randomNumber(0, positionChars.length - 1);
password += positionChars.charAt(randomCharIndex);
}
return password;
}
async generatePassphrase(options: any): Promise<string> {
const o = Object.assign({}, DefaultOptions, options);
if (o.numWords == null || o.numWords <= 2) {
o.numWords = DefaultOptions.numWords;
}
if (o.wordSeparator == null || o.wordSeparator.length === 0 || o.wordSeparator.length > 1) {
o.wordSeparator = " ";
}
if (o.capitalize == null) {
o.capitalize = false;
}
if (o.includeNumber == null) {
o.includeNumber = false;
}
const listLength = EEFLongWordList.length - 1;
const wordList = new Array(o.numWords);
for (let i = 0; i < o.numWords; i++) {
const wordIndex = await this.cryptoService.randomNumber(0, listLength);
if (o.capitalize) {
wordList[i] = this.capitalize(EEFLongWordList[wordIndex]);
} else {
wordList[i] = EEFLongWordList[wordIndex];
}
}
if (o.includeNumber) {
await this.appendRandomNumberToRandomWord(wordList);
}
return wordList.join(o.wordSeparator);
}
async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> {
let options = await this.stateService.getPasswordGenerationOptions();
if (options == null) {
options = Object.assign({}, DefaultOptions);
} else {
options = Object.assign({}, DefaultOptions, options);
}
await this.stateService.setPasswordGenerationOptions(options);
const enforcedOptions = await this.enforcePasswordGeneratorPoliciesOnOptions(options);
options = enforcedOptions[0];
return [options, enforcedOptions[1]];
}
async enforcePasswordGeneratorPoliciesOnOptions(
options: any,
): Promise<[any, PasswordGeneratorPolicyOptions]> {
let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions();
if (enforcedPolicyOptions != null) {
if (options.length < enforcedPolicyOptions.minLength) {
options.length = enforcedPolicyOptions.minLength;
}
if (enforcedPolicyOptions.useUppercase) {
options.uppercase = true;
}
if (enforcedPolicyOptions.useLowercase) {
options.lowercase = true;
}
if (enforcedPolicyOptions.useNumbers) {
options.number = true;
}
if (options.minNumber < enforcedPolicyOptions.numberCount) {
options.minNumber = enforcedPolicyOptions.numberCount;
}
if (enforcedPolicyOptions.useSpecial) {
options.special = true;
}
if (options.minSpecial < enforcedPolicyOptions.specialCount) {
options.minSpecial = enforcedPolicyOptions.specialCount;
}
// Must normalize these fields because the receiving call expects all options to pass the current rules
if (options.minSpecial + options.minNumber > options.length) {
options.minSpecial = options.length - options.minNumber;
}
if (options.numWords < enforcedPolicyOptions.minNumberWords) {
options.numWords = enforcedPolicyOptions.minNumberWords;
}
if (enforcedPolicyOptions.capitalize) {
options.capitalize = true;
}
if (enforcedPolicyOptions.includeNumber) {
options.includeNumber = true;
}
// Force default type if password/passphrase selected via policy
if (
enforcedPolicyOptions.defaultType === "password" ||
enforcedPolicyOptions.defaultType === "passphrase"
) {
options.type = enforcedPolicyOptions.defaultType;
}
} else {
// UI layer expects an instantiated object to prevent more explicit null checks
enforcedPolicyOptions = new PasswordGeneratorPolicyOptions();
}
return [options, enforcedPolicyOptions];
}
async getPasswordGeneratorPolicyOptions(): Promise<PasswordGeneratorPolicyOptions> {
const policies: Policy[] =
this.policyService == null
? null
: await this.policyService.getAll(PolicyType.PasswordGenerator);
let enforcedOptions: PasswordGeneratorPolicyOptions = null;
if (policies == null || policies.length === 0) {
return enforcedOptions;
}
policies.forEach((currentPolicy) => {
if (!currentPolicy.enabled || currentPolicy.data == null) {
return;
}
if (enforcedOptions == null) {
enforcedOptions = new PasswordGeneratorPolicyOptions();
}
// Password wins in multi-org collisions
if (currentPolicy.data.defaultType != null && enforcedOptions.defaultType !== "password") {
enforcedOptions.defaultType = currentPolicy.data.defaultType;
}
if (
currentPolicy.data.minLength != null &&
currentPolicy.data.minLength > enforcedOptions.minLength
) {
enforcedOptions.minLength = currentPolicy.data.minLength;
}
if (currentPolicy.data.useUpper) {
enforcedOptions.useUppercase = true;
}
if (currentPolicy.data.useLower) {
enforcedOptions.useLowercase = true;
}
if (currentPolicy.data.useNumbers) {
enforcedOptions.useNumbers = true;
}
if (
currentPolicy.data.minNumbers != null &&
currentPolicy.data.minNumbers > enforcedOptions.numberCount
) {
enforcedOptions.numberCount = currentPolicy.data.minNumbers;
}
if (currentPolicy.data.useSpecial) {
enforcedOptions.useSpecial = true;
}
if (
currentPolicy.data.minSpecial != null &&
currentPolicy.data.minSpecial > enforcedOptions.specialCount
) {
enforcedOptions.specialCount = currentPolicy.data.minSpecial;
}
if (
currentPolicy.data.minNumberWords != null &&
currentPolicy.data.minNumberWords > enforcedOptions.minNumberWords
) {
enforcedOptions.minNumberWords = currentPolicy.data.minNumberWords;
}
if (currentPolicy.data.capitalize) {
enforcedOptions.capitalize = true;
}
if (currentPolicy.data.includeNumber) {
enforcedOptions.includeNumber = true;
}
});
return enforcedOptions;
}
async saveOptions(options: any) {
await this.stateService.setPasswordGenerationOptions(options);
}
async getHistory(): Promise<GeneratedPasswordHistory[]> {
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
return new Array<GeneratedPasswordHistory>();
}
if ((await this.stateService.getDecryptedPasswordGenerationHistory()) == null) {
const encrypted = await this.stateService.getEncryptedPasswordGenerationHistory();
const decrypted = await this.decryptHistory(encrypted);
await this.stateService.setDecryptedPasswordGenerationHistory(decrypted);
}
const passwordGenerationHistory =
await this.stateService.getDecryptedPasswordGenerationHistory();
return passwordGenerationHistory != null
? passwordGenerationHistory
: new Array<GeneratedPasswordHistory>();
}
async addHistory(password: string): Promise<any> {
// Cannot add new history if no key is available
const hasKey = await this.cryptoService.hasKey();
if (!hasKey) {
return;
}
const currentHistory = await this.getHistory();
// Prevent duplicates
if (this.matchesPrevious(password, currentHistory)) {
return;
}
currentHistory.unshift(new GeneratedPasswordHistory(password, Date.now()));
// Remove old items.
if (currentHistory.length > MaxPasswordsInHistory) {
currentHistory.pop();
}
const newHistory = await this.encryptHistory(currentHistory);
return await this.stateService.setEncryptedPasswordGenerationHistory(newHistory);
}
async clear(userId?: string): Promise<any> {
await this.stateService.setEncryptedPasswordGenerationHistory(null, { userId: userId });
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
}
passwordStrength(password: string, userInputs: string[] = null): zxcvbn.ZXCVBNResult {
if (password == null || password.length === 0) {
return null;
}
let globalUserInputs = ["bitwarden", "bit", "warden"];
if (userInputs != null && userInputs.length > 0) {
globalUserInputs = globalUserInputs.concat(userInputs);
}
// Use a hash set to get rid of any duplicate user inputs
const finalUserInputs = Array.from(new Set(globalUserInputs));
const result = zxcvbn(password, finalUserInputs);
return result;
}
normalizeOptions(options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) {
options.minLowercase = 0;
options.minUppercase = 0;
if (!options.length || options.length < 5) {
options.length = 5;
} else if (options.length > 128) {
options.length = 128;
}
if (options.length < enforcedPolicyOptions.minLength) {
options.length = enforcedPolicyOptions.minLength;
}
if (!options.minNumber) {
options.minNumber = 0;
} else if (options.minNumber > options.length) {
options.minNumber = options.length;
} else if (options.minNumber > 9) {
options.minNumber = 9;
}
if (options.minNumber < enforcedPolicyOptions.numberCount) {
options.minNumber = enforcedPolicyOptions.numberCount;
}
if (!options.minSpecial) {
options.minSpecial = 0;
} else if (options.minSpecial > options.length) {
options.minSpecial = options.length;
} else if (options.minSpecial > 9) {
options.minSpecial = 9;
}
if (options.minSpecial < enforcedPolicyOptions.specialCount) {
options.minSpecial = enforcedPolicyOptions.specialCount;
}
if (options.minSpecial + options.minNumber > options.length) {
options.minSpecial = options.length - options.minNumber;
}
if (options.numWords == null || options.length < 3) {
options.numWords = 3;
} else if (options.numWords > 20) {
options.numWords = 20;
}
if (options.numWords < enforcedPolicyOptions.minNumberWords) {
options.numWords = enforcedPolicyOptions.minNumberWords;
}
if (options.wordSeparator != null && options.wordSeparator.length > 1) {
options.wordSeparator = options.wordSeparator[0];
}
this.sanitizePasswordLength(options, false);
}
private capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
private async appendRandomNumberToRandomWord(wordList: string[]) {
if (wordList == null || wordList.length <= 0) {
return;
}
const index = await this.cryptoService.randomNumber(0, wordList.length - 1);
const num = await this.cryptoService.randomNumber(0, 9);
wordList[index] = wordList[index] + num;
}
private async encryptHistory(
history: GeneratedPasswordHistory[],
): Promise<GeneratedPasswordHistory[]> {
if (history == null || history.length === 0) {
return Promise.resolve([]);
}
const promises = history.map(async (item) => {
const encrypted = await this.cryptoService.encrypt(item.password);
return new GeneratedPasswordHistory(encrypted.encryptedString, item.date);
});
return await Promise.all(promises);
}
private async decryptHistory(
history: GeneratedPasswordHistory[],
): Promise<GeneratedPasswordHistory[]> {
if (history == null || history.length === 0) {
return Promise.resolve([]);
}
const promises = history.map(async (item) => {
const decrypted = await this.cryptoService.decryptToUtf8(new EncString(item.password));
return new GeneratedPasswordHistory(decrypted, item.date);
});
return await Promise.all(promises);
}
private matchesPrevious(password: string, history: GeneratedPasswordHistory[]): boolean {
if (history == null || history.length === 0) {
return false;
}
return history[history.length - 1].password === password;
}
// ref: https://stackoverflow.com/a/12646864/1090359
private async shuffleArray(array: string[]) {
for (let i = array.length - 1; i > 0; i--) {
const j = await this.cryptoService.randomNumber(0, i);
[array[i], array[j]] = [array[j], array[i]];
}
}
private sanitizePasswordLength(options: any, forGeneration: boolean) {
let minUppercaseCalc = 0;
let minLowercaseCalc = 0;
let minNumberCalc: number = options.minNumber;
let minSpecialCalc: number = options.minSpecial;
if (options.uppercase && options.minUppercase <= 0) {
minUppercaseCalc = 1;
} else if (!options.uppercase) {
minUppercaseCalc = 0;
}
if (options.lowercase && options.minLowercase <= 0) {
minLowercaseCalc = 1;
} else if (!options.lowercase) {
minLowercaseCalc = 0;
}
if (options.number && options.minNumber <= 0) {
minNumberCalc = 1;
} else if (!options.number) {
minNumberCalc = 0;
}
if (options.special && options.minSpecial <= 0) {
minSpecialCalc = 1;
} else if (!options.special) {
minSpecialCalc = 0;
}
// This should never happen but is a final safety net
if (!options.length || options.length < 1) {
options.length = 10;
}
const minLength: number = minUppercaseCalc + minLowercaseCalc + minNumberCalc + minSpecialCalc;
// Normalize and Generation both require this modification
if (options.length < minLength) {
options.length = minLength;
}
// Apply other changes if the options object passed in is for generation
if (forGeneration) {
options.minUppercase = minUppercaseCalc;
options.minLowercase = minLowercaseCalc;
options.minNumber = minNumberCalc;
options.minSpecial = minSpecialCalc;
}
}
}

View File

@@ -1,247 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { OrganizationService } from "../abstractions/organization.service";
import { PolicyService as PolicyServiceAbstraction } from "../abstractions/policy.service";
import { StateService } from "../abstractions/state.service";
import { OrganizationUserStatusType } from "../enums/organizationUserStatusType";
import { OrganizationUserType } from "../enums/organizationUserType";
import { PolicyType } from "../enums/policyType";
import { PolicyData } from "../models/data/policyData";
import { MasterPasswordPolicyOptions } from "../models/domain/masterPasswordPolicyOptions";
import { Organization } from "../models/domain/organization";
import { Policy } from "../models/domain/policy";
import { ResetPasswordPolicyOptions } from "../models/domain/resetPasswordPolicyOptions";
import { ListResponse } from "../models/response/listResponse";
import { PolicyResponse } from "../models/response/policyResponse";
export class PolicyService implements PolicyServiceAbstraction {
policyCache: Policy[];
constructor(
private stateService: StateService,
private organizationService: OrganizationService,
private apiService: ApiService,
) {}
async clearCache(): Promise<void> {
await this.stateService.setDecryptedPolicies(null);
}
async getAll(type?: PolicyType, userId?: string): Promise<Policy[]> {
let response: Policy[] = [];
const decryptedPolicies = await this.stateService.getDecryptedPolicies({ userId: userId });
if (decryptedPolicies != null) {
response = decryptedPolicies;
} else {
const diskPolicies = await this.stateService.getEncryptedPolicies({ userId: userId });
for (const id in diskPolicies) {
// eslint-disable-next-line
if (diskPolicies.hasOwnProperty(id)) {
response.push(new Policy(diskPolicies[id]));
}
}
await this.stateService.setDecryptedPolicies(response, { userId: userId });
}
if (type != null) {
return response.filter((policy) => policy.type === type);
} else {
return response;
}
}
async getPolicyForOrganization(policyType: PolicyType, organizationId: string): Promise<Policy> {
const org = await this.organizationService.get(organizationId);
if (org?.isProviderUser) {
const orgPolicies = await this.apiService.getPolicies(organizationId);
const policy = orgPolicies.data.find((p) => p.organizationId === organizationId);
if (policy == null) {
return null;
}
return new Policy(new PolicyData(policy));
}
const policies = await this.getAll(policyType);
return policies.find((p) => p.organizationId === organizationId);
}
async replace(policies: { [id: string]: PolicyData }): Promise<any> {
await this.stateService.setDecryptedPolicies(null);
await this.stateService.setEncryptedPolicies(policies);
}
async clear(userId?: string): Promise<any> {
await this.stateService.setDecryptedPolicies(null, { userId: userId });
await this.stateService.setEncryptedPolicies(null, { userId: userId });
}
async getMasterPasswordPoliciesForInvitedUsers(
orgId: string,
): Promise<MasterPasswordPolicyOptions> {
const userId = await this.stateService.getUserId();
const response = await this.apiService.getPoliciesByInvitedUser(orgId, userId);
const policies = await this.mapPoliciesFromToken(response);
return this.getMasterPasswordPolicyOptions(policies);
}
async getMasterPasswordPolicyOptions(policies?: Policy[]): Promise<MasterPasswordPolicyOptions> {
let enforcedOptions: MasterPasswordPolicyOptions = null;
if (policies == null) {
policies = await this.getAll(PolicyType.MasterPassword);
} else {
policies = policies.filter((p) => p.type === PolicyType.MasterPassword);
}
if (policies == null || policies.length === 0) {
return enforcedOptions;
}
policies.forEach((currentPolicy) => {
if (!currentPolicy.enabled || currentPolicy.data == null) {
return;
}
if (enforcedOptions == null) {
enforcedOptions = new MasterPasswordPolicyOptions();
}
if (
currentPolicy.data.minComplexity != null &&
currentPolicy.data.minComplexity > enforcedOptions.minComplexity
) {
enforcedOptions.minComplexity = currentPolicy.data.minComplexity;
}
if (
currentPolicy.data.minLength != null &&
currentPolicy.data.minLength > enforcedOptions.minLength
) {
enforcedOptions.minLength = currentPolicy.data.minLength;
}
if (currentPolicy.data.requireUpper) {
enforcedOptions.requireUpper = true;
}
if (currentPolicy.data.requireLower) {
enforcedOptions.requireLower = true;
}
if (currentPolicy.data.requireNumbers) {
enforcedOptions.requireNumbers = true;
}
if (currentPolicy.data.requireSpecial) {
enforcedOptions.requireSpecial = true;
}
});
return enforcedOptions;
}
evaluateMasterPassword(
passwordStrength: number,
newPassword: string,
enforcedPolicyOptions: MasterPasswordPolicyOptions,
): boolean {
if (enforcedPolicyOptions == null) {
return true;
}
if (
enforcedPolicyOptions.minComplexity > 0 &&
enforcedPolicyOptions.minComplexity > passwordStrength
) {
return false;
}
if (
enforcedPolicyOptions.minLength > 0 &&
enforcedPolicyOptions.minLength > newPassword.length
) {
return false;
}
if (enforcedPolicyOptions.requireUpper && newPassword.toLocaleLowerCase() === newPassword) {
return false;
}
if (enforcedPolicyOptions.requireLower && newPassword.toLocaleUpperCase() === newPassword) {
return false;
}
if (enforcedPolicyOptions.requireNumbers && !/[0-9]/.test(newPassword)) {
return false;
}
// eslint-disable-next-line
if (enforcedPolicyOptions.requireSpecial && !/[!@#$%\^&*]/g.test(newPassword)) {
return false;
}
return true;
}
getResetPasswordPolicyOptions(
policies: Policy[],
orgId: string,
): [ResetPasswordPolicyOptions, boolean] {
const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions();
if (policies == null || orgId == null) {
return [resetPasswordPolicyOptions, false];
}
const policy = policies.find(
(p) => p.organizationId === orgId && p.type === PolicyType.ResetPassword && p.enabled,
);
resetPasswordPolicyOptions.autoEnrollEnabled = policy?.data?.autoEnrollEnabled ?? false;
return [resetPasswordPolicyOptions, policy?.enabled ?? false];
}
mapPoliciesFromToken(policiesResponse: ListResponse<PolicyResponse>): Policy[] {
if (policiesResponse == null || policiesResponse.data == null) {
return null;
}
const policiesData = policiesResponse.data.map((p) => new PolicyData(p));
return policiesData.map((p) => new Policy(p));
}
async policyAppliesToUser(
policyType: PolicyType,
policyFilter?: (policy: Policy) => boolean,
userId?: string,
) {
const policies = await this.getAll(policyType, userId);
const organizations = await this.organizationService.getAll(userId);
let filteredPolicies;
if (policyFilter != null) {
filteredPolicies = policies.filter((p) => p.enabled && policyFilter(p));
} else {
filteredPolicies = policies.filter((p) => p.enabled);
}
const policySet = new Set(filteredPolicies.map((p) => p.organizationId));
return organizations.some(
(o) =>
o.enabled &&
o.status >= OrganizationUserStatusType.Accepted &&
o.usePolicies &&
!this.isExcemptFromPolicies(o, policyType) &&
policySet.has(o.id),
);
}
private isExcemptFromPolicies(organization: Organization, policyType: PolicyType) {
if (policyType === PolicyType.MaximumVaultTimeout) {
return organization.type === OrganizationUserType.Owner;
}
return organization.isExemptFromPolicies;
}
}

View File

@@ -1,496 +0,0 @@
import * as program from "commander";
import * as inquirer from "inquirer";
import Separator from "inquirer/lib/objects/separator";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { PasswordGenerationService } from "@/jslib/common/src/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { PolicyService } from "@/jslib/common/src/abstractions/policy.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { TwoFactorProviderType } from "@/jslib/common/src/enums/twoFactorProviderType";
import { NodeUtils } from "@/jslib/common/src/misc/nodeUtils";
import { Utils } from "@/jslib/common/src/misc/utils";
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
} from "@/jslib/common/src/models/domain/logInCredentials";
import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor";
import { TwoFactorEmailRequest } from "@/jslib/common/src/models/request/twoFactorEmailRequest";
import { UpdateTempPasswordRequest } from "@/jslib/common/src/models/request/updateTempPasswordRequest";
import { ErrorResponse } from "@/jslib/common/src/models/response/errorResponse";
import { Response } from "../models/response";
import { MessageResponse } from "../models/response/messageResponse";
export class LoginCommand {
protected validatedParams: () => Promise<any>;
protected success: () => Promise<MessageResponse>;
protected logout: () => Promise<void>;
protected canInteract: boolean;
protected clientId: string;
protected clientSecret: string;
protected email: string;
constructor(
protected authService: AuthService,
protected apiService: ApiService,
protected i18nService: I18nService,
protected environmentService: EnvironmentService,
protected passwordGenerationService: PasswordGenerationService,
protected cryptoFunctionService: CryptoFunctionService,
protected platformUtilsService: PlatformUtilsService,
protected stateService: StateService,
protected cryptoService: CryptoService,
protected policyService: PolicyService,
protected twoFactorService: TwoFactorService,
clientId: string,
) {
this.clientId = clientId;
}
async run(email: string, password: string, options: program.OptionValues) {
this.canInteract = process.env.BW_NOINTERACTION !== "true";
let clientId: string = null;
let clientSecret: string = null;
let selectedProvider: any = null;
if (options.apikey != null) {
const apiIdentifiers = await this.apiIdentifiers();
clientId = apiIdentifiers.clientId;
clientSecret = apiIdentifiers.clientSecret;
} else {
if ((email == null || email === "") && this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "input",
name: "email",
message: "Email address:",
});
email = answer.email;
}
if (email == null || email.trim() === "") {
return Response.badRequest("Email address is required.");
}
if (email.indexOf("@") === -1) {
return Response.badRequest("Email address is invalid.");
}
this.email = email;
if (password == null || password === "") {
if (options.passwordfile) {
password = await NodeUtils.readFirstLine(options.passwordfile);
} else if (options.passwordenv && process.env[options.passwordenv]) {
password = process.env[options.passwordenv];
} else if (this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "password",
name: "password",
message: "Master password:",
});
password = answer.password;
}
}
if (password == null || password === "") {
return Response.badRequest("Master password is required.");
}
}
let twoFactorToken: string = options.code;
let twoFactorMethod: TwoFactorProviderType = null;
try {
if (options.method != null) {
twoFactorMethod = parseInt(options.method, null);
}
} catch (e) {
return Response.error("Invalid two-step login method.");
}
const twoFactor =
twoFactorToken == null
? null
: new TokenRequestTwoFactor(twoFactorMethod, twoFactorToken, false);
try {
if (this.validatedParams != null) {
await this.validatedParams();
}
let response: AuthResult = null;
if (clientId != null && clientSecret != null) {
response = await this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
} else {
response = await this.authService.logIn(
new PasswordLogInCredentials(email, password, null, twoFactor),
);
}
if (response.captchaSiteKey) {
const credentials = new PasswordLogInCredentials(email, password);
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);
// Error Response
if (handledResponse instanceof Response) {
return handledResponse;
} else {
response = handledResponse;
}
}
if (response.requiresTwoFactor) {
const twoFactorProviders = this.twoFactorService.getSupportedProviders(null);
if (twoFactorProviders.length === 0) {
return Response.badRequest("No providers available for this client.");
}
if (twoFactorMethod != null) {
try {
selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0];
} catch (e) {
return Response.error("Invalid two-step login method.");
}
}
if (selectedProvider == null) {
if (twoFactorProviders.length === 1) {
selectedProvider = twoFactorProviders[0];
} else if (this.canInteract) {
const twoFactorOptions: (string | Separator)[] = twoFactorProviders.map((p) => p.name);
twoFactorOptions.push(new inquirer.Separator());
twoFactorOptions.push("Cancel");
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "list",
name: "method",
message: "Two-step login method:",
choices: twoFactorOptions,
});
const i = twoFactorOptions.indexOf(answer.method);
if (i === twoFactorOptions.length - 1) {
return Response.error("Login failed.");
}
selectedProvider = twoFactorProviders[i];
}
if (selectedProvider == null) {
return Response.error("Login failed. No provider selected.");
}
}
if (
twoFactorToken == null &&
response.twoFactorProviders.size > 1 &&
selectedProvider.type === TwoFactorProviderType.Email
) {
const emailReq = new TwoFactorEmailRequest();
emailReq.email = this.authService.email;
emailReq.masterPasswordHash = this.authService.masterPasswordHash;
await this.apiService.postTwoFactorEmail(emailReq);
}
if (twoFactorToken == null) {
if (this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "input",
name: "token",
message: "Two-step login code:",
});
twoFactorToken = answer.token;
}
if (twoFactorToken == null || twoFactorToken === "") {
return Response.badRequest("Code is required.");
}
}
response = await this.authService.logInTwoFactor(
new TokenRequestTwoFactor(selectedProvider.type, twoFactorToken),
null,
);
}
if (response.captchaSiteKey) {
const twoFactorRequest = new TokenRequestTwoFactor(selectedProvider.type, twoFactorToken);
const handledResponse = await this.handleCaptchaRequired(twoFactorRequest);
// Error Response
if (handledResponse instanceof Response) {
return handledResponse;
} else {
response = handledResponse;
}
}
if (response.requiresTwoFactor) {
return Response.error("Login failed.");
}
if (response.resetMasterPassword) {
return Response.error(
"In order to log in with SSO from the CLI, you must first log in" +
" through the web vault to set your master password.",
);
}
// Handle Updating Temp Password if NOT using an API Key for authentication
if (response.forcePasswordReset && clientId == null && clientSecret == null) {
return await this.updateTempPassword();
}
return await this.handleSuccessResponse();
} catch (e) {
return Response.error(e);
}
}
private async handleSuccessResponse(): Promise<Response> {
if (this.success != null) {
const res = await this.success();
return Response.success(res);
} else {
const res = new MessageResponse("You are logged in!", null);
return Response.success(res);
}
}
private async updateTempPassword(error?: string): Promise<Response> {
// If no interaction available, alert user to use web vault
if (!this.canInteract) {
await this.logout();
this.authService.logOut(() => {
/* Do nothing */
});
return Response.error(
new MessageResponse(
"An organization administrator recently changed your master password. In order to access the vault, you must update your master password now via the web vault. You have been logged out.",
null,
),
);
}
if (this.email == null || this.email === "undefined") {
this.email = await this.stateService.getEmail();
}
// Get New Master Password
const baseMessage =
"An organization administrator recently changed your master password. In order to access the vault, you must update your master password now.\n" +
"Master password: ";
const firstMessage = error != null ? error + baseMessage : baseMessage;
const mp: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: "password",
name: "password",
message: firstMessage,
});
const masterPassword = mp.password;
// Master Password Validation
if (masterPassword == null || masterPassword === "") {
return this.updateTempPassword("Master password is required.\n");
}
if (masterPassword.length < 8) {
return this.updateTempPassword("Master password must be at least 8 characters long.\n");
}
// Strength & Policy Validation
const strengthResult = this.passwordGenerationService.passwordStrength(
masterPassword,
this.getPasswordStrengthUserInput(),
);
// Get New Master Password Re-type
const reTypeMessage = "Re-type New Master password (Strength: " + strengthResult.score + ")";
const retype: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: "password",
name: "password",
message: reTypeMessage,
});
const masterPasswordRetype = retype.password;
// Re-type Validation
if (masterPassword !== masterPasswordRetype) {
return this.updateTempPassword("Master password confirmation does not match.\n");
}
// Get Hint (optional)
const hint: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: "input",
name: "input",
message: "Master Password Hint (optional):",
});
const masterPasswordHint = hint.input;
// Retrieve details for key generation
const enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions();
const kdf = await this.stateService.getKdfType();
const kdfIterations = await this.stateService.getKdfIterations();
if (
enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
strengthResult.score,
masterPassword,
enforcedPolicyOptions,
)
) {
return this.updateTempPassword(
"Your new master password does not meet the policy requirements.\n",
);
}
try {
// Create new key and hash new password
const newKey = await this.cryptoService.makeKey(
masterPassword,
this.email.trim().toLowerCase(),
kdf,
kdfIterations,
);
const newPasswordHash = await this.cryptoService.hashPassword(masterPassword, newKey);
// Grab user's current enc key
const userEncKey = await this.cryptoService.getEncKey();
// Create new encKey for the User
const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey);
// Create request
const request = new UpdateTempPasswordRequest();
request.key = newEncKey[1].encryptedString;
request.newMasterPasswordHash = newPasswordHash;
request.masterPasswordHint = masterPasswordHint;
// Update user's password
await this.apiService.putUpdateTempPassword(request);
return this.handleSuccessResponse();
} catch (e) {
await this.logout();
this.authService.logOut(() => {
/* Do nothing */
});
return Response.error(e);
}
}
private async handleCaptchaRequired(
twoFactorRequest: TokenRequestTwoFactor,
credentials: PasswordLogInCredentials = null,
): Promise<AuthResult | Response> {
const badCaptcha = Response.badRequest(
"Your authentication request has been flagged and will require user interaction to proceed.\n" +
"Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" +
"(https://bitwarden.com/help/cli-auth-challenges)",
);
try {
const captchaClientSecret = await this.apiClientSecret(true);
if (Utils.isNullOrWhitespace(captchaClientSecret)) {
return badCaptcha;
}
let authResultResponse: AuthResult = null;
if (credentials != null) {
credentials.captchaToken = captchaClientSecret;
credentials.twoFactor = twoFactorRequest;
authResultResponse = await this.authService.logIn(credentials);
} else {
authResultResponse = await this.authService.logInTwoFactor(
twoFactorRequest,
captchaClientSecret,
);
}
return authResultResponse;
} catch (e) {
if (
e instanceof ErrorResponse ||
(e.constructor.name === ErrorResponse.name &&
(e as ErrorResponse).message.includes("Captcha is invalid"))
) {
return badCaptcha;
} else {
return Response.error(e);
}
}
}
private getPasswordStrengthUserInput() {
let userInput: string[] = [];
const atPosition = this.email.indexOf("@");
if (atPosition > -1) {
userInput = userInput.concat(
this.email
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/),
);
}
return userInput;
}
private async apiClientId(): Promise<string> {
let clientId: string = null;
const storedClientId: string = process.env.BW_CLIENTID;
if (storedClientId == null) {
if (this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "input",
name: "clientId",
message: "client_id:",
});
clientId = answer.clientId;
} else {
clientId = null;
}
} else {
clientId = storedClientId;
}
return clientId;
}
private async apiClientSecret(isAdditionalAuthentication = false): Promise<string> {
const additionalAuthenticationMessage = "Additional authentication required.\nAPI key ";
let clientSecret: string = null;
const storedClientSecret: string = this.clientSecret || process.env.BW_CLIENTSECRET;
if (this.canInteract && storedClientSecret == null) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "input",
name: "clientSecret",
message:
(isAdditionalAuthentication ? additionalAuthenticationMessage : "") + "client_secret:",
});
clientSecret = answer.clientSecret;
} else {
clientSecret = storedClientSecret;
}
return clientSecret;
}
private async apiIdentifiers(): Promise<{ clientId: string; clientSecret: string }> {
return {
clientId: await this.apiClientId(),
clientSecret: await this.apiClientSecret(),
};
}
}

78
package-lock.json generated
View File

@@ -39,8 +39,7 @@
"proper-lockfile": "4.1.2", "proper-lockfile": "4.1.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tldjs": "2.3.1", "tldjs": "2.3.1",
"zone.js": "0.13.1", "zone.js": "0.13.1"
"zxcvbn": "4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-eslint/eslint-plugin-template": "17.2.0", "@angular-eslint/eslint-plugin-template": "17.2.0",
@@ -60,7 +59,6 @@
"@types/node-forge": "1.3.11", "@types/node-forge": "1.3.11",
"@types/proper-lockfile": "4.1.4", "@types/proper-lockfile": "4.1.4",
"@types/tldjs": "2.3.4", "@types/tldjs": "2.3.4",
"@types/zxcvbn": "4.4.4",
"@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0", "@typescript-eslint/parser": "5.62.0",
"clean-webpack-plugin": "4.0.0", "clean-webpack-plugin": "4.0.0",
@@ -87,6 +85,7 @@
"husky": "9.0.10", "husky": "9.0.10",
"jest": "29.7.0", "jest": "29.7.0",
"jest-junit": "16.0.0", "jest-junit": "16.0.0",
"jest-mock-extended": "3.0.7",
"jest-preset-angular": "13.1.1", "jest-preset-angular": "13.1.1",
"lint-staged": "15.2.10", "lint-staged": "15.2.10",
"mini-css-extract-plugin": "2.9.1", "mini-css-extract-plugin": "2.9.1",
@@ -101,6 +100,7 @@
"ts-jest": "29.2.5", "ts-jest": "29.2.5",
"ts-loader": "9.5.1", "ts-loader": "9.5.1",
"tsconfig-paths-webpack-plugin": "4.1.0", "tsconfig-paths-webpack-plugin": "4.1.0",
"type-fest": "3.13.1",
"typescript": "4.9.5", "typescript": "4.9.5",
"typescript-transform-paths": "3.5.1", "typescript-transform-paths": "3.5.1",
"webpack": "5.94.0", "webpack": "5.94.0",
@@ -4669,13 +4669,6 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
"dev": true,
"license": "MIT"
},
"node_modules/@popperjs/core": { "node_modules/@popperjs/core": {
"version": "2.11.8", "version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -4687,6 +4680,13 @@
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
} }
}, },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
"dev": true,
"license": "MIT"
},
"node_modules/@sinclair/typebox": { "node_modules/@sinclair/typebox": {
"version": "0.27.8", "version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -5286,12 +5286,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/zxcvbn": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.4.tgz",
"integrity": "sha512-Tuk4q7q0DnpzyJDI4aMeghGuFu2iS1QAdKpabn8JfbtfGmVDUgvZv1I7mEjP61Bvnp3ljKCC8BE6YYSTNxmvRQ==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.62.0", "version": "5.62.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
@@ -9524,6 +9518,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/electron-store/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"dev": true,
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.673", "version": "1.4.673",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.673.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.673.tgz",
@@ -13806,6 +13812,19 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
} }
}, },
"node_modules/jest-mock-extended": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-3.0.7.tgz",
"integrity": "sha512-7lsKdLFcW9B9l5NzZ66S/yTQ9k8rFtnwYdCNuRU/81fqDWicNDVhitTSPnrGmNeNm0xyw0JHexEOShrIKRCIRQ==",
"dev": true,
"dependencies": {
"ts-essentials": "^10.0.0"
},
"peerDependencies": {
"jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0",
"typescript": "^3.0.0 || ^4.0.0 || ^5.0.0"
}
},
"node_modules/jest-pnp-resolver": { "node_modules/jest-pnp-resolver": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz",
@@ -19902,6 +19921,20 @@
"typescript": ">=4.2.0" "typescript": ">=4.2.0"
} }
}, },
"node_modules/ts-essentials": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-10.0.2.tgz",
"integrity": "sha512-Xwag0TULqriaugXqVdDiGZ5wuZpqABZlpwQ2Ho4GDyiu/R2Xjkp/9+zcFxL7uzeLl/QCPrflnvpVYyS3ouT7Zw==",
"dev": true,
"peerDependencies": {
"typescript": ">=4.5.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/ts-jest": { "node_modules/ts-jest": {
"version": "29.2.5", "version": "29.2.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz",
@@ -20131,12 +20164,12 @@
} }
}, },
"node_modules/type-fest": { "node_modules/type-fest": {
"version": "2.19.0", "version": "3.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=12.20" "node": ">=14.16"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -21408,11 +21441,6 @@
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
} }
},
"node_modules/zxcvbn": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/zxcvbn/-/zxcvbn-4.4.2.tgz",
"integrity": "sha512-Bq0B+ixT/DMyG8kgX2xWcI5jUvCwqrMxSFam7m0lAf78nf04hv6lNCsyLYdyYTrCVMqNDY/206K7eExYCeSyUQ=="
} }
} }
} }

View File

@@ -73,6 +73,8 @@
"@angular-eslint/eslint-plugin-template": "17.2.0", "@angular-eslint/eslint-plugin-template": "17.2.0",
"@angular-eslint/template-parser": "17.2.0", "@angular-eslint/template-parser": "17.2.0",
"@angular/compiler-cli": "16.2.12", "@angular/compiler-cli": "16.2.12",
"@electron/notarize": "2.2.1",
"@electron/rebuild": "3.6.0",
"@fluffy-spoon/substitute": "1.208.0", "@fluffy-spoon/substitute": "1.208.0",
"@microsoft/microsoft-graph-types": "2.40.0", "@microsoft/microsoft-graph-types": "2.40.0",
"@ngtools/webpack": "16.2.12", "@ngtools/webpack": "16.2.12",
@@ -85,7 +87,6 @@
"@types/node-forge": "1.3.11", "@types/node-forge": "1.3.11",
"@types/proper-lockfile": "4.1.4", "@types/proper-lockfile": "4.1.4",
"@types/tldjs": "2.3.4", "@types/tldjs": "2.3.4",
"@types/zxcvbn": "4.4.4",
"@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0", "@typescript-eslint/parser": "5.62.0",
"clean-webpack-plugin": "4.0.0", "clean-webpack-plugin": "4.0.0",
@@ -97,8 +98,6 @@
"electron": "28.2.0", "electron": "28.2.0",
"electron-builder": "24.9.1", "electron-builder": "24.9.1",
"electron-log": "5.2.0", "electron-log": "5.2.0",
"@electron/notarize": "2.2.1",
"@electron/rebuild": "3.6.0",
"electron-reload": "2.0.0-alpha.1", "electron-reload": "2.0.0-alpha.1",
"electron-store": "8.1.0", "electron-store": "8.1.0",
"electron-updater": "6.1.7", "electron-updater": "6.1.7",
@@ -114,6 +113,7 @@
"husky": "9.0.10", "husky": "9.0.10",
"jest": "29.7.0", "jest": "29.7.0",
"jest-junit": "16.0.0", "jest-junit": "16.0.0",
"jest-mock-extended": "3.0.7",
"jest-preset-angular": "13.1.1", "jest-preset-angular": "13.1.1",
"lint-staged": "15.2.10", "lint-staged": "15.2.10",
"mini-css-extract-plugin": "2.9.1", "mini-css-extract-plugin": "2.9.1",
@@ -128,6 +128,7 @@
"ts-jest": "29.2.5", "ts-jest": "29.2.5",
"ts-loader": "9.5.1", "ts-loader": "9.5.1",
"tsconfig-paths-webpack-plugin": "4.1.0", "tsconfig-paths-webpack-plugin": "4.1.0",
"type-fest": "3.13.1",
"typescript": "4.9.5", "typescript": "4.9.5",
"typescript-transform-paths": "3.5.1", "typescript-transform-paths": "3.5.1",
"webpack": "5.94.0", "webpack": "5.94.0",
@@ -166,8 +167,7 @@
"proper-lockfile": "4.1.2", "proper-lockfile": "4.1.2",
"rxjs": "7.8.1", "rxjs": "7.8.1",
"tldjs": "2.3.1", "tldjs": "2.3.1",
"zone.js": "0.13.1", "zone.js": "0.13.1"
"zxcvbn": "4.4.2"
}, },
"engines": { "engines": {
"node": "~18", "node": "~18",

View File

@@ -14,8 +14,6 @@ import { EnvironmentService } from "@/jslib/common/src/services/environment.serv
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service"; import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
import { NoopMessagingService } from "@/jslib/common/src/services/noopMessaging.service"; import { NoopMessagingService } from "@/jslib/common/src/services/noopMessaging.service";
import { OrganizationService } from "@/jslib/common/src/services/organization.service"; import { OrganizationService } from "@/jslib/common/src/services/organization.service";
import { PasswordGenerationService } from "@/jslib/common/src/services/passwordGeneration.service";
import { PolicyService } from "@/jslib/common/src/services/policy.service";
import { TokenService } from "@/jslib/common/src/services/token.service"; import { TokenService } from "@/jslib/common/src/services/token.service";
import { CliPlatformUtilsService } from "@/jslib/node/src/cli/services/cliPlatformUtils.service"; import { CliPlatformUtilsService } from "@/jslib/node/src/cli/services/cliPlatformUtils.service";
import { ConsoleLogService } from "@/jslib/node/src/cli/services/consoleLog.service"; import { ConsoleLogService } from "@/jslib/node/src/cli/services/consoleLog.service";
@@ -39,6 +37,8 @@ const packageJson = require("../package.json");
export class Main { export class Main {
dataFilePath: string; dataFilePath: string;
logService: ConsoleLogService; logService: ConsoleLogService;
program: Program;
messagingService: NoopMessagingService; messagingService: NoopMessagingService;
storageService: LowdbStorageService; storageService: LowdbStorageService;
secureStorageService: StorageServiceAbstraction; secureStorageService: StorageServiceAbstraction;
@@ -53,10 +53,7 @@ export class Main {
cryptoFunctionService: NodeCryptoFunctionService; cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService; authService: AuthService;
syncService: SyncService; syncService: SyncService;
passwordGenerationService: PasswordGenerationService;
policyService: PolicyService;
keyConnectorService: KeyConnectorService; keyConnectorService: KeyConnectorService;
program: Program;
stateService: StateService; stateService: StateService;
stateMigrationService: StateMigrationService; stateMigrationService: StateMigrationService;
organizationService: OrganizationService; organizationService: OrganizationService;
@@ -187,18 +184,6 @@ export class Main {
this.stateService, this.stateService,
); );
this.policyService = new PolicyService(
this.stateService,
this.organizationService,
this.apiService,
);
this.passwordGenerationService = new PasswordGenerationService(
this.cryptoService,
this.policyService,
this.stateService,
);
this.program = new Program(this); this.program = new Program(this);
} }

View File

@@ -0,0 +1,66 @@
import { mock, MockProxy } from "jest-mock-extended";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { LoginCommand } from "./login.command";
const clientId = "test_client_id";
const clientSecret = "test_client_secret";
// Mock responses from the inquirer prompt
// This combines both prompt results into a single object which is returned both times
jest.mock("inquirer", () => ({
createPromptModule: () => () => ({
clientId,
clientSecret,
}),
}));
describe("LoginCommand", () => {
let authService: MockProxy<AuthService>;
let loginCommand: LoginCommand;
beforeEach(() => {
// reset env variables
delete process.env.BW_CLIENTID;
delete process.env.BW_CLIENTSECRET;
authService = mock();
loginCommand = new LoginCommand(authService);
});
it("uses client id and secret stored in environment variables", async () => {
process.env.BW_CLIENTID = clientId;
process.env.BW_CLIENTSECRET = clientSecret;
authService.logIn.mockResolvedValue(new AuthResult()); // logging in with api key does not set any flag on the authResult
const result = await loginCommand.run();
expect(authService.logIn).toHaveBeenCalledWith(new ApiLogInCredentials(clientId, clientSecret));
expect(result).toMatchObject({
data: {
title: "You are logged in!",
},
success: true,
});
});
it("uses client id and secret prompted from the user", async () => {
authService.logIn.mockResolvedValue(new AuthResult()); // logging in with api key does not set any flag on the authResult
const result = await loginCommand.run();
expect(authService.logIn).toHaveBeenCalledWith(new ApiLogInCredentials(clientId, clientSecret));
expect(result).toMatchObject({
data: {
title: "You are logged in!",
},
success: true,
});
});
});

View File

@@ -0,0 +1,88 @@
import * as inquirer from "inquirer";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { Response } from "@/jslib/node/src/cli/models/response";
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
import { Utils } from "../../jslib/common/src/misc/utils";
export class LoginCommand {
private canInteract: boolean;
constructor(private authService: AuthService) {}
async run() {
this.canInteract = process.env.BW_NOINTERACTION !== "true";
const { clientId, clientSecret } = await this.apiIdentifiers();
if (Utils.isNullOrWhitespace(clientId)) {
return Response.error("Client ID is required.");
}
if (Utils.isNullOrWhitespace(clientSecret)) {
return Response.error("Client Secret is required.");
}
try {
await this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
const res = new MessageResponse("You are logged in!", null);
return Response.success(res);
} catch (e) {
return Response.error(e);
}
}
private async apiClientId(): Promise<string> {
let clientId: string = null;
const storedClientId: string = process.env.BW_CLIENTID;
if (storedClientId == null) {
if (this.canInteract) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "input",
name: "clientId",
message: "client_id:",
});
clientId = answer.clientId;
} else {
clientId = null;
}
} else {
clientId = storedClientId;
}
return clientId;
}
private async apiClientSecret(): Promise<string> {
let clientSecret: string = null;
const storedClientSecret = process.env.BW_CLIENTSECRET;
if (this.canInteract && storedClientSecret == null) {
const answer: inquirer.Answers = await inquirer.createPromptModule({
output: process.stderr,
})({
type: "input",
name: "clientSecret",
message: "client_secret:",
});
clientSecret = answer.clientSecret;
} else {
clientSecret = storedClientSecret;
}
return clientSecret;
}
private async apiIdentifiers(): Promise<{ clientId: string; clientSecret: string }> {
return {
clientId: await this.apiClientId(),
clientSecret: await this.apiClientSecret(),
};
}
}

View File

@@ -5,7 +5,6 @@ import { Command, OptionValues } from "commander";
import { Utils } from "@/jslib/common/src/misc/utils"; import { Utils } from "@/jslib/common/src/misc/utils";
import { BaseProgram } from "@/jslib/node/src/cli/baseProgram"; import { BaseProgram } from "@/jslib/node/src/cli/baseProgram";
import { LoginCommand } from "@/jslib/node/src/cli/commands/login.command";
import { LogoutCommand } from "@/jslib/node/src/cli/commands/logout.command"; import { LogoutCommand } from "@/jslib/node/src/cli/commands/logout.command";
import { UpdateCommand } from "@/jslib/node/src/cli/commands/update.command"; import { UpdateCommand } from "@/jslib/node/src/cli/commands/update.command";
import { Response } from "@/jslib/node/src/cli/models/response"; import { Response } from "@/jslib/node/src/cli/models/response";
@@ -15,6 +14,7 @@ import { Main } from "./bwdc";
import { ClearCacheCommand } from "./commands/clearCache.command"; import { ClearCacheCommand } from "./commands/clearCache.command";
import { ConfigCommand } from "./commands/config.command"; import { ConfigCommand } from "./commands/config.command";
import { LastSyncCommand } from "./commands/lastSync.command"; import { LastSyncCommand } from "./commands/lastSync.command";
import { LoginCommand } from "./commands/login.command";
import { SyncCommand } from "./commands/sync.command"; import { SyncCommand } from "./commands/sync.command";
import { TestCommand } from "./commands/test.command"; import { TestCommand } from "./commands/test.command";
@@ -92,20 +92,7 @@ export class Program extends BaseProgram {
}) })
.action(async (clientId: string, clientSecret: string, options: OptionValues) => { .action(async (clientId: string, clientSecret: string, options: OptionValues) => {
await this.exitIfAuthed(); await this.exitIfAuthed();
const command = new LoginCommand( const command = new LoginCommand(this.main.authService);
this.main.authService,
this.main.apiService,
this.main.i18nService,
this.main.environmentService,
this.main.passwordGenerationService,
this.main.cryptoFunctionService,
this.main.platformUtilsService,
this.main.stateService,
this.main.cryptoService,
this.main.policyService,
this.main.twoFactorService,
"connector",
);
if (!Utils.isNullOrWhitespace(clientId)) { if (!Utils.isNullOrWhitespace(clientId)) {
process.env.BW_CLIENTID = clientId; process.env.BW_CLIENTID = clientId;
@@ -114,8 +101,7 @@ export class Program extends BaseProgram {
process.env.BW_CLIENTSECRET = clientSecret; process.env.BW_CLIENTSECRET = clientSecret;
} }
options = Object.assign(options ?? {}, { apikey: true }); // force apikey use const response = await command.run();
const response = await command.run(null, null, options);
this.processResponse(response); this.processResponse(response);
}); });