diff --git a/jslib b/jslib
index 25917faf..ca61e13b 160000
--- a/jslib
+++ b/jslib
@@ -1 +1 @@
-Subproject commit 25917faf9153106d7ef249f949a00b901ffbd33c
+Subproject commit ca61e13b57e32f1b4526c415573a03444247123a
diff --git a/src/app/accounts/login.component.html b/src/app/accounts/apiKey.component.html
similarity index 68%
rename from src/app/accounts/login.component.html
rename to src/app/accounts/apiKey.component.html
index 6ac1318b..7e497fa6 100644
--- a/src/app/accounts/login.component.html
+++ b/src/app/accounts/apiKey.component.html
@@ -8,14 +8,15 @@
-
-
+
+
@@ -25,10 +26,6 @@
{{'logIn' | i18n}}
-
-
diff --git a/src/app/tabs/settings.component.ts b/src/app/tabs/settings.component.ts
index 98f9ac3c..90e6337a 100644
--- a/src/app/tabs/settings.component.ts
+++ b/src/app/tabs/settings.component.ts
@@ -37,9 +37,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
okta = new OktaConfiguration();
oneLogin = new OneLoginConfiguration();
sync = new SyncConfiguration();
- organizationId: string;
directoryOptions: any[];
- organizationOptions: any[];
constructor(private i18nService: I18nService, private configurationService: ConfigurationService,
private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone,
@@ -55,15 +53,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
}
async ngOnInit() {
- this.organizationOptions = [{ name: this.i18nService.t('select'), value: null }];
- const orgs = await this.stateService.get
('profileOrganizations');
- if (orgs != null) {
- for (const org of orgs) {
- this.organizationOptions.push({ name: org.name, value: org.id });
- }
- }
-
- this.organizationId = await this.configurationService.getOrganizationId();
this.directory = await this.configurationService.getDirectoryType();
this.ldap = (await this.configurationService.getDirectory(DirectoryType.Ldap)) ||
this.ldap;
@@ -87,7 +76,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
if (this.ldap != null && this.ldap.ad) {
this.ldap.pagedSearch = true;
}
- await this.configurationService.saveOrganizationId(this.organizationId);
await this.configurationService.saveDirectoryType(this.directory);
await this.configurationService.saveDirectory(DirectoryType.Ldap, this.ldap);
await this.configurationService.saveDirectory(DirectoryType.GSuite, this.gsuite);
diff --git a/src/bwdc.ts b/src/bwdc.ts
index 437ebf31..e1e9a61f 100644
--- a/src/bwdc.ts
+++ b/src/bwdc.ts
@@ -3,7 +3,7 @@ import * as path from 'path';
import { LogLevelType } from 'jslib/enums/logLevelType';
-import { AuthService } from 'jslib/services/auth.service';
+import { AuthService } from './services/auth.service';
import { ConfigurationService } from './services/configuration.service';
import { I18nService } from './services/i18n.service';
@@ -14,6 +14,7 @@ import { SyncService } from './services/sync.service';
import { CliPlatformUtilsService } from 'jslib/cli/services/cliPlatformUtils.service';
import { ConsoleLogService } from 'jslib/cli/services/consoleLog.service';
+import { ApiKeyService } from 'jslib/services/apiKey.service';
import { AppIdService } from 'jslib/services/appId.service';
import { ConstantsService } from 'jslib/services/constants.service';
import { ContainerService } from 'jslib/services/container.service';
@@ -47,6 +48,7 @@ export class Main {
appIdService: AppIdService;
apiService: NodeApiService;
environmentService: EnvironmentService;
+ apiKeyService: ApiKeyService;
userService: UserService;
containerService: ContainerService;
cryptoFunctionService: NodeCryptoFunctionService;
@@ -91,11 +93,12 @@ export class Main {
this.apiService = new NodeApiService(this.tokenService, this.platformUtilsService,
async (expired: boolean) => await this.logout());
this.environmentService = new EnvironmentService(this.apiService, this.storageService, null);
+ this.apiKeyService = new ApiKeyService(this.tokenService, this.storageService);
this.userService = new UserService(this.tokenService, this.storageService);
this.containerService = new ContainerService(this.cryptoService);
this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, this.tokenService,
this.appIdService, this.i18nService, this.platformUtilsService, this.messagingService, null,
- this.logService, false);
+ this.logService, this.apiKeyService, false);
this.configurationService = new ConfigurationService(this.storageService, this.secureStorageService,
process.env.BITWARDENCLI_CONNECTOR_PLAINTEXT_SECRETS !== 'true');
this.syncService = new SyncService(this.configurationService, this.logService, this.cryptoFunctionService,
@@ -110,10 +113,8 @@ export class Main {
}
async logout() {
- await Promise.all([
- this.tokenService.clearToken(),
- this.userService.clear(),
- ]);
+ await this.tokenService.clearToken();
+ await this.apiKeyService.clear();
}
private async init() {
diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json
index 4dcbb59f..34264765 100644
--- a/src/locales/en/messages.json
+++ b/src/locales/en/messages.json
@@ -20,12 +20,30 @@
"emailRequired": {
"message": "Email address is required."
},
+ "clientIdRequired": {
+ "message": "Client Id is required."
+ },
+ "invalidClientId": {
+ "message": "Invalid Client Id provided."
+ },
+ "clientSecretRequired": {
+ "message": "Client Secret is required."
+ },
+ "orgApiKeyRequired": {
+ "message": "Api Key must belong to an Organization"
+ },
+ "failedToSaveCredentials": {
+ "message": "Failed to save credentials"
+ },
"invalidEmail": {
"message": "Invalid email address."
},
"masterPassRequired": {
"message": "Master password is required."
},
+ "missingRequiredInput": {
+ "message": "Missing required input."
+ },
"unexpectedError": {
"message": "An unexpected error has occurred."
},
@@ -575,7 +593,7 @@
"message": "Welcome to the Bitwarden Directory Connector"
},
"logInDesc": {
- "message": "Log in as an organization admin user below."
+ "message": "Log in with an organization API key below."
},
"dirConfigIncomplete": {
"message": "Directory configuration incomplete."
diff --git a/src/program.ts b/src/program.ts
index dd4013de..b7893391 100644
--- a/src/program.ts
+++ b/src/program.ts
@@ -16,9 +16,12 @@ import { UpdateCommand } from 'jslib/cli/commands/update.command';
import { BaseProgram } from 'jslib/cli/baseProgram';
+import { ApiKeyService } from 'jslib/abstractions/apiKey.service';
import { Response } from 'jslib/cli/models/response';
import { StringResponse } from 'jslib/cli/models/response/stringResponse';
+import { Utils } from 'jslib/misc/utils';
+
const writeLn = (s: string, finalLine: boolean = false, error: boolean = false) => {
const stream = error ? process.stderr : process.stdout;
if (finalLine && process.platform === 'win32') {
@@ -29,8 +32,11 @@ const writeLn = (s: string, finalLine: boolean = false, error: boolean = false)
};
export class Program extends BaseProgram {
+ private apiKeyService: ApiKeyService;
+
constructor(private main: Main) {
super(main.userService, writeLn);
+ this.apiKeyService = main.apiKeyService;
}
async run() {
@@ -86,34 +92,26 @@ export class Program extends BaseProgram {
});
program
- .command('login [email] [password]')
- .description('Log into a user account.')
- .option('--method ', 'Two-step login method.')
- .option('--code ', 'Two-step login code.')
- .option('--sso', 'Log in with Single-Sign On.')
- .option('--passwordenv ', 'Read password from the named environment variable.')
- .option('--passwordfile ', 'Read password from first line of the named file.')
- .on('--help', () => {
- writeLn('\n Notes:');
- writeLn('');
- writeLn(' See docs for valid `method` enum values.');
- writeLn('');
- writeLn(' Examples:');
- writeLn('');
- writeLn(' bwdc login');
- writeLn(' bwdc login john@example.com myPassword321');
- writeLn(' bwdc login john@example.com myPassword321 --method 1 --code 249213');
- writeLn(' bwdc login john@example.com --passwordfile passwd.txt --method 1 --code 249213');
- writeLn(' bwdc login john@example.com --passwordenv MY_PASSWD --method 1 --code 249213');
- writeLn(' bwdc login --sso');
- writeLn('', true);
+ .command('login [clientId] [clientSecret]')
+ .description('Log into an organization account.', {
+ clientId: 'Client_id part of your organization\'s API key',
+ clientSecret: 'Client_secret part of your organization\'s API key',
})
- .action(async (email: string, password: string, options: program.OptionValues) => {
+ .action(async (clientId: string, clientSecret: string, options: program.OptionValues) => {
await this.exitIfAuthed();
const command = new LoginCommand(this.main.authService, this.main.apiService, this.main.i18nService,
this.main.environmentService, this.main.passwordGenerationService, this.main.cryptoFunctionService,
this.main.platformUtilsService, 'connector');
- const response = await command.run(email, password, options);
+
+ if (!Utils.isNullOrWhitespace(clientId)) {
+ process.env.BW_CLIENTID = clientId;
+ }
+ if (!Utils.isNullOrWhitespace(clientSecret)) {
+ process.env.BW_CLIENTSECRET = clientSecret;
+ }
+
+ options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
+ const response = await command.run(null, null, options);
this.processResponse(response);
});
@@ -284,4 +282,20 @@ export class Program extends BaseProgram {
program.outputHelp();
}
}
+
+ async exitIfAuthed() {
+ const authed = await this.apiKeyService.isAuthenticated();
+ if (authed) {
+ const type = await this.apiKeyService.getEntityType();
+ const id = await this.apiKeyService.getEntityId();
+ this.processResponse(Response.error('You are already logged in as ' + type + '.' + id + '.'), true);
+ }
+ }
+
+ async exitIfNotAuthed() {
+ const authed = await this.apiKeyService.isAuthenticated();
+ if (!authed) {
+ this.processResponse(Response.error('You are not logged in.'), true);
+ }
+ }
}
diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts
new file mode 100644
index 00000000..eba7c50c
--- /dev/null
+++ b/src/services/auth.service.ts
@@ -0,0 +1,56 @@
+import { ApiService } from 'jslib/abstractions/api.service';
+import { ApiKeyService } from 'jslib/abstractions/apiKey.service';
+import { AppIdService } from 'jslib/abstractions/appId.service';
+import { CryptoService } from 'jslib/abstractions/crypto.service';
+import { I18nService } from 'jslib/abstractions/i18n.service';
+import { LogService } from 'jslib/abstractions/log.service';
+import { MessagingService } from 'jslib/abstractions/messaging.service';
+import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
+import { TokenService } from 'jslib/abstractions/token.service';
+import { UserService } from 'jslib/abstractions/user.service';
+import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
+
+import { AuthService as AuthServiceBase } from 'jslib/services/auth.service';
+
+import { AuthResult } from 'jslib/models/domain';
+import { DeviceRequest } from 'jslib/models/request/deviceRequest';
+import { TokenRequest } from 'jslib/models/request/tokenRequest';
+import { IdentityTokenResponse } from 'jslib/models/response/identityTokenResponse';
+
+export class AuthService extends AuthServiceBase {
+
+ constructor(cryptoService: CryptoService, apiService: ApiService, userService: UserService,
+ tokenService: TokenService, appIdService: AppIdService, i18nService: I18nService,
+ platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
+ vaultTimeoutService: VaultTimeoutService, logService: LogService, private apiKeyService: ApiKeyService,
+ setCryptoKeys = true) {
+ super(cryptoService, apiService, userService, tokenService, appIdService, i18nService, platformUtilsService,
+ messagingService, vaultTimeoutService, logService, setCryptoKeys);
+ }
+
+ async logInApiKey(clientId: string, clientSecret: string): Promise {
+ this.selectedTwoFactorProviderType = null;
+ if (clientId.startsWith('organization')) {
+ return await this.organizationLogInHelper(clientId, clientSecret);
+ }
+ return await super.logInApiKey(clientId, clientSecret);
+ }
+
+ private async organizationLogInHelper(clientId: string, clientSecret: string) {
+ const appId = await this.appIdService.getAppId();
+ const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
+ const request = new TokenRequest(null, null, [clientId, clientSecret], null,
+ null, false, deviceRequest);
+
+ const response = await this.apiService.postIdentityToken(request);
+ const result = new AuthResult();
+ result.twoFactor = !(response as any).accessToken;
+
+ const tokenResponse = response as IdentityTokenResponse;
+ result.resetMasterPassword = tokenResponse.resetMasterPassword;
+ await this.tokenService.setToken(tokenResponse.accessToken);
+ await this.apiKeyService.setInformation(clientId);
+
+ return result;
+ }
+}
diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts
index 26f649c3..d64c86af 100644
--- a/src/services/sync.service.ts
+++ b/src/services/sync.service.ts
@@ -4,9 +4,7 @@ import { GroupEntry } from '../models/groupEntry';
import { SyncConfiguration } from '../models/syncConfiguration';
import { UserEntry } from '../models/userEntry';
-import { ImportDirectoryRequest } from 'jslib/models/request/importDirectoryRequest';
-import { ImportDirectoryRequestGroup } from 'jslib/models/request/importDirectoryRequestGroup';
-import { ImportDirectoryRequestUser } from 'jslib/models/request/importDirectoryRequestUser';
+import { OrganizationImportRequest } from 'jslib/models/request/organizationImportRequest';
import { ApiService } from 'jslib/abstractions/api.service';
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
@@ -58,7 +56,7 @@ export class SyncService {
}
if (test || (!syncConfig.overwriteExisting &&
- (groups == null || groups.length === 0) && (users == null || users.length === 0))) {
+ (groups == null || groups.length === 0) && (users == null || users.length === 0))) {
if (!test) {
await this.saveSyncTimes(syncConfig, now);
}
@@ -89,7 +87,7 @@ export class SyncService {
const lastHash = await this.configurationService.getLastSyncHash();
if (lastHash == null || (hash !== lastHash && hashLegacy !== lastHash)) {
- await this.apiService.postImportDirectory(orgId, req);
+ await this.apiService.postPublicImportDirectory(req);
await this.configurationService.saveLastSyncHash(hash);
} else {
groups = null;
@@ -145,36 +143,26 @@ export class SyncService {
}
}
- private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean,
- overwriteExisting: boolean, largeImport: boolean): ImportDirectoryRequest {
- const model = new ImportDirectoryRequest();
- model.overwriteExisting = overwriteExisting;
- model.largeImport = largeImport;
-
- if (groups != null) {
- for (const g of groups) {
- const ig = new ImportDirectoryRequestGroup();
- ig.name = g.name;
- ig.externalId = g.externalId;
- ig.users = Array.from(g.userMemberExternalIds);
- model.groups.push(ig);
- }
- }
-
- if (users != null) {
- for (const u of users) {
- const iu = new ImportDirectoryRequestUser();
- iu.email = u.email;
- if (iu.email != null) {
- iu.email = iu.email.trim().toLowerCase();
- }
- iu.externalId = u.externalId;
- iu.deleted = u.deleted || (removeDisabled && u.disabled);
- model.users.push(iu);
- }
- }
-
- return model;
+ private buildRequest(groups: GroupEntry[], users: UserEntry[], removeDisabled: boolean, overwriteExisting: boolean,
+ largeImport: boolean = false) {
+ return new OrganizationImportRequest({
+ groups: (groups ?? []).map(g => {
+ return {
+ name: g.name,
+ externalId: g.externalId,
+ memberExternalIds: Array.from(g.userMemberExternalIds),
+ };
+ }),
+ users: (users ?? []).map(u => {
+ return {
+ email: u.email,
+ externalId: u.externalId,
+ deleted: u.deleted || (removeDisabled && u.disabled),
+ };
+ }),
+ overwriteExisting: overwriteExisting,
+ largeImport: largeImport,
+ });
}
private async saveSyncTimes(syncConfig: SyncConfiguration, time: Date) {