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:
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
78
package-lock.json
generated
@@ -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=="
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -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",
|
||||||
|
|||||||
19
src/bwdc.ts
19
src/bwdc.ts
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
src/commands/login.command.spec.ts
Normal file
66
src/commands/login.command.spec.ts
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
88
src/commands/login.command.ts
Normal file
88
src/commands/login.command.ts
Normal 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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user