From 56d05af07a69ddf2368847ffc42b712702e3a991 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 2 Jun 2021 13:43:18 -0500 Subject: [PATCH] Use organization api key for auth (#121) * Use api key for login * Remove user login and organization setting * Override Api authentication to expect organization keys * Linter fixes * Use public API The organization api key is valid only in the public api scope * Use organization api key in CLI utility * Serialize storageService writes * Prefer multiple awaits to .then chains * Initial PR review * Do not treat api key inputs as passwords This conforms with how they are handled in CLI/web * Update jslib * PR feedback --- jslib | 2 +- ...n.component.html => apiKey.component.html} | 15 ++-- src/app/accounts/apiKey.component.ts | 80 +++++++++++++++++++ src/app/accounts/login.component.ts | 57 ------------- src/app/accounts/sso.component.html | 30 ------- src/app/accounts/sso.component.ts | 61 -------------- .../two-factor-options.component.html | 26 ------ .../accounts/two-factor-options.component.ts | 21 ----- src/app/accounts/two-factor.component.html | 70 ---------------- src/app/accounts/two-factor.component.ts | 64 --------------- src/app/app-routing.module.ts | 8 +- src/app/app.component.ts | 12 +-- src/app/app.module.ts | 11 +-- src/app/services/auth-guard.service.ts | 6 +- src/app/services/launch-guard.service.ts | 6 +- src/app/services/services.module.ts | 9 ++- src/app/tabs/settings.component.html | 11 --- src/app/tabs/settings.component.ts | 12 --- src/bwdc.ts | 13 +-- src/locales/en/messages.json | 20 ++++- src/program.ts | 60 ++++++++------ src/services/auth.service.ts | 56 +++++++++++++ src/services/sync.service.ts | 58 ++++++-------- 23 files changed, 248 insertions(+), 460 deletions(-) rename src/app/accounts/{login.component.html => apiKey.component.html} (68%) create mode 100644 src/app/accounts/apiKey.component.ts delete mode 100644 src/app/accounts/login.component.ts delete mode 100644 src/app/accounts/sso.component.html delete mode 100644 src/app/accounts/sso.component.ts delete mode 100644 src/app/accounts/two-factor-options.component.html delete mode 100644 src/app/accounts/two-factor-options.component.ts delete mode 100644 src/app/accounts/two-factor.component.html delete mode 100644 src/app/accounts/two-factor.component.ts create mode 100644 src/services/auth.service.ts 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 @@
{{'logIn' | i18n}}
- - + +
- - + +
@@ -25,10 +26,6 @@ {{'logIn' | i18n}} -
- -
- - - - - - diff --git a/src/app/accounts/sso.component.ts b/src/app/accounts/sso.component.ts deleted file mode 100644 index 8555927d..00000000 --- a/src/app/accounts/sso.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Component } from '@angular/core'; - -import { - ActivatedRoute, - Router, -} from '@angular/router'; - -import { ApiService } from 'jslib/abstractions/api.service'; -import { AuthService } from 'jslib/abstractions/auth.service'; -import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service'; -import { EnvironmentService } from 'jslib/abstractions/environment.service'; -import { I18nService } from 'jslib/abstractions/i18n.service'; -import { MessagingService } from 'jslib/abstractions/messaging.service'; -import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service'; -import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; -import { StateService } from 'jslib/abstractions/state.service'; -import { StorageService } from 'jslib/abstractions/storage.service'; - -import { SsoComponent as BaseSsoComponent } from 'jslib/angular/components/sso.component'; - -@Component({ - selector: 'app-sso', - templateUrl: 'sso.component.html', -}) -export class SsoComponent extends BaseSsoComponent { - showMasterPassRedirect: boolean = false; - - constructor(authService: AuthService, router: Router, - i18nService: I18nService, route: ActivatedRoute, - storageService: StorageService, stateService: StateService, - platformUtilsService: PlatformUtilsService, apiService: ApiService, - cryptoFunctionService: CryptoFunctionService, - passwordGenerationService: PasswordGenerationService, private messagingService: MessagingService, - private environmentService: EnvironmentService) { - super(authService, router, i18nService, route, storageService, stateService, platformUtilsService, - apiService, cryptoFunctionService, passwordGenerationService); - this.successRoute = '/tabs/dashboard'; - this.redirectUri = 'bwdc://sso-callback'; - this.clientId = 'connector'; - this.onSuccessfulLoginChangePasswordNavigate = this.redirectSetMasterPass; - } - - async redirectSetMasterPass() { - this.showMasterPassRedirect = true; - } - - launchWebVault() { - const webUrl = this.environmentService.webVaultUrl == null ? 'https://vault.bitwarden.com' : - this.environmentService.webVaultUrl; - - this.platformUtilsService.launchUri(webUrl); - } - - async logOut() { - const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('logOutConfirmation'), - this.i18nService.t('logOut'), this.i18nService.t('logOut'), this.i18nService.t('cancel')); - if (confirmed) { - this.messagingService.send('logout'); - } - } -} diff --git a/src/app/accounts/two-factor-options.component.html b/src/app/accounts/two-factor-options.component.html deleted file mode 100644 index ad2bdd04..00000000 --- a/src/app/accounts/two-factor-options.component.html +++ /dev/null @@ -1,26 +0,0 @@ - diff --git a/src/app/accounts/two-factor-options.component.ts b/src/app/accounts/two-factor-options.component.ts deleted file mode 100644 index 6db002ad..00000000 --- a/src/app/accounts/two-factor-options.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; - -import { AuthService } from 'jslib/abstractions/auth.service'; -import { I18nService } from 'jslib/abstractions/i18n.service'; -import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; - -import { - TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent, -} from 'jslib/angular/components/two-factor-options.component'; - -@Component({ - selector: 'app-two-factor-options', - templateUrl: 'two-factor-options.component.html', -}) -export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { - constructor(authService: AuthService, router: Router, - i18nService: I18nService, platformUtilsService: PlatformUtilsService) { - super(authService, router, i18nService, platformUtilsService, window); - } -} diff --git a/src/app/accounts/two-factor.component.html b/src/app/accounts/two-factor.component.html deleted file mode 100644 index 74987aeb..00000000 --- a/src/app/accounts/two-factor.component.html +++ /dev/null @@ -1,70 +0,0 @@ -
-
-
-
-
-
{{title}}
-
- -

- {{'enterVerificationCodeApp' | i18n}} -

-

- {{'enterVerificationCodeEmail' | i18n : twoFactorEmail}} -

-
- - -
-
- -

{{'insertYubiKey' | i18n}}

-

-
- - -
-
- -
- -
-
-
-
- - -
-
- -

{{'noTwoStepProviders' | i18n}}

-

{{'noTwoStepProviders2' | i18n}}

-
- - {{'cancel' | i18n}} -
-
- -
-
-
-
- diff --git a/src/app/accounts/two-factor.component.ts b/src/app/accounts/two-factor.component.ts deleted file mode 100644 index c7cd2ba1..00000000 --- a/src/app/accounts/two-factor.component.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - Component, - ComponentFactoryResolver, - ViewChild, - ViewContainerRef, -} from '@angular/core'; - -import { - ActivatedRoute, - Router, -} from '@angular/router'; - -import { TwoFactorOptionsComponent } from './two-factor-options.component'; - -import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType'; - -import { ApiService } from 'jslib/abstractions/api.service'; -import { AuthService } from 'jslib/abstractions/auth.service'; -import { EnvironmentService } from 'jslib/abstractions/environment.service'; -import { I18nService } from 'jslib/abstractions/i18n.service'; -import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; -import { StateService } from 'jslib/abstractions/state.service'; -import { StorageService } from 'jslib/abstractions/storage.service'; - -import { ModalComponent } from 'jslib/angular/components/modal.component'; -import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/components/two-factor.component'; - -@Component({ - selector: 'app-two-factor', - templateUrl: 'two-factor.component.html', -}) -export class TwoFactorComponent extends BaseTwoFactorComponent { - @ViewChild('twoFactorOptions', { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef; - - constructor(authService: AuthService, router: Router, - i18nService: I18nService, apiService: ApiService, - platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, - private componentFactoryResolver: ComponentFactoryResolver, stateService: StateService, - storageService: StorageService, route: ActivatedRoute) { - super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService, - stateService, storageService, route); - } - - async ngOnInit() { - await super.ngOnInit(); - super.successRoute = '/tabs/dashboard'; - } - - anotherMethod() { - const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); - const modal = this.twoFactorOptionsModal.createComponent(factory).instance; - const childComponent = modal.show(TwoFactorOptionsComponent, - this.twoFactorOptionsModal); - - childComponent.onProviderSelected.subscribe(async (provider: TwoFactorProviderType) => { - modal.close(); - this.selectedProviderType = provider; - await this.init(); - }); - childComponent.onRecoverSelected.subscribe(() => { - modal.close(); - }); - } -} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 82fda652..54550c20 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -7,9 +7,7 @@ import { import { AuthGuardService } from './services/auth-guard.service'; import { LaunchGuardService } from './services/launch-guard.service'; -import { LoginComponent } from './accounts/login.component'; -import { SsoComponent } from './accounts/sso.component'; -import { TwoFactorComponent } from './accounts/two-factor.component'; +import { ApiKeyComponent } from './accounts/apiKey.component'; import { DashboardComponent } from './tabs/dashboard.component'; import { MoreComponent } from './tabs/more.component'; import { SettingsComponent } from './tabs/settings.component'; @@ -19,11 +17,9 @@ const routes: Routes = [ { path: '', redirectTo: '/login', pathMatch: 'full' }, { path: 'login', - component: LoginComponent, + component: ApiKeyComponent, canActivate: [LaunchGuardService], }, - { path: '2fa', component: TwoFactorComponent }, - { path: 'sso', component: SsoComponent }, { path: 'tabs', component: TabsComponent, diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 803584d2..838ff684 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -73,12 +73,6 @@ export class AppComponent implements OnInit { this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { this.ngZone.run(async () => { switch (message.command) { - case 'loggedIn': - if (await this.userService.isAuthenticated()) { - const profile = await this.apiService.getProfile(); - this.stateService.save('profileOrganizations', profile.organizations); - } - break; case 'syncScheduleStarted': case 'syncScheduleStopped': this.stateService.save('syncingDir', message.command === 'syncScheduleStarted'); @@ -140,10 +134,8 @@ export class AppComponent implements OnInit { private async logOut(expired: boolean) { const userId = await this.userService.getUserId(); - await Promise.all([ - this.tokenService.clearToken(), - this.userService.clear(), - ]); + await this.tokenService.clearToken(); + await this.userService.clear(); this.authService.logOut(async () => { if (expired) { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 196522fc..5cfe2472 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -17,11 +17,8 @@ import { CalloutComponent } from 'jslib/angular/components/callout.component'; import { IconComponent } from 'jslib/angular/components/icon.component'; import { ModalComponent } from 'jslib/angular/components/modal.component'; +import { ApiKeyComponent } from './accounts/apiKey.component'; import { EnvironmentComponent } from './accounts/environment.component'; -import { LoginComponent } from './accounts/login.component'; -import { SsoComponent } from './accounts/sso.component'; -import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; -import { TwoFactorComponent } from './accounts/two-factor.component'; import { DashboardComponent } from './tabs/dashboard.component'; import { MoreComponent } from './tabs/more.component'; import { SettingsComponent } from './tabs/settings.component'; @@ -51,6 +48,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe'; declarations: [ A11yTitleDirective, ApiActionDirective, + ApiKeyComponent, AppComponent, AutofocusDirective, BlurClickDirective, @@ -61,22 +59,17 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe'; FallbackSrcDirective, I18nPipe, IconComponent, - LoginComponent, ModalComponent, MoreComponent, SearchCiphersPipe, SettingsComponent, - SsoComponent, StopClickDirective, StopPropDirective, TabsComponent, - TwoFactorComponent, - TwoFactorOptionsComponent, ], entryComponents: [ EnvironmentComponent, ModalComponent, - TwoFactorOptionsComponent, ], providers: [], bootstrap: [AppComponent], diff --git a/src/app/services/auth-guard.service.ts b/src/app/services/auth-guard.service.ts index f890b991..b0989fcb 100644 --- a/src/app/services/auth-guard.service.ts +++ b/src/app/services/auth-guard.service.ts @@ -3,17 +3,17 @@ import { CanActivate, Router, } from '@angular/router'; +import { ApiKeyService } from 'jslib/abstractions/apiKey.service'; import { MessagingService } from 'jslib/abstractions/messaging.service'; -import { UserService } from 'jslib/abstractions/user.service'; @Injectable() export class AuthGuardService implements CanActivate { - constructor(private userService: UserService, private router: Router, + constructor(private apiKeyService: ApiKeyService, private router: Router, private messagingService: MessagingService) { } async canActivate() { - const isAuthed = await this.userService.isAuthenticated(); + const isAuthed = await this.apiKeyService.isAuthenticated(); if (!isAuthed) { this.messagingService.send('logout'); return false; diff --git a/src/app/services/launch-guard.service.ts b/src/app/services/launch-guard.service.ts index 705553ad..a4e3a34f 100644 --- a/src/app/services/launch-guard.service.ts +++ b/src/app/services/launch-guard.service.ts @@ -4,14 +4,14 @@ import { Router, } from '@angular/router'; -import { UserService } from 'jslib/abstractions/user.service'; +import { ApiKeyService } from 'jslib/abstractions/apiKey.service'; @Injectable() export class LaunchGuardService implements CanActivate { - constructor(private userService: UserService, private router: Router) { } + constructor(private apiKeyService: ApiKeyService, private router: Router) { } async canActivate() { - const isAuthed = await this.userService.isAuthenticated(); + const isAuthed = await this.apiKeyService.isAuthenticated(); if (!isAuthed) { return true; } diff --git a/src/app/services/services.module.ts b/src/app/services/services.module.ts index 5b2367e0..0025849a 100644 --- a/src/app/services/services.module.ts +++ b/src/app/services/services.module.ts @@ -22,8 +22,8 @@ import { BroadcasterService } from 'jslib/angular/services/broadcaster.service'; import { ValidationService } from 'jslib/angular/services/validation.service'; import { ApiService } from 'jslib/services/api.service'; +import { ApiKeyService } from 'jslib/services/apiKey.service'; import { AppIdService } from 'jslib/services/appId.service'; -import { AuthService } from 'jslib/services/auth.service'; import { ConstantsService } from 'jslib/services/constants.service'; import { ContainerService } from 'jslib/services/container.service'; import { CryptoService } from 'jslib/services/crypto.service'; @@ -36,6 +36,7 @@ import { TokenService } from 'jslib/services/token.service'; import { UserService } from 'jslib/services/user.service'; import { ApiService as ApiServiceAbstraction } from 'jslib/abstractions/api.service'; +import { ApiKeyService as ApiKeyServiceAbstraction } from 'jslib/abstractions/apiKey.service'; import { AuthService as AuthServiceAbstraction } from 'jslib/abstractions/auth.service'; import { CryptoService as CryptoServiceAbstraction } from 'jslib/abstractions/crypto.service'; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from 'jslib/abstractions/cryptoFunction.service'; @@ -53,6 +54,8 @@ import { StorageService as StorageServiceAbstraction } from 'jslib/abstractions/ import { TokenService as TokenServiceAbstraction } from 'jslib/abstractions/token.service'; import { UserService as UserServiceAbstraction } from 'jslib/abstractions/user.service'; +import { AuthService } from '../../services/auth.service'; + const logService = new ElectronLogService(); const i18nService = new I18nService(window.navigator.language, './locales'); const stateService = new StateService(); @@ -70,9 +73,10 @@ const apiService = new ApiService(tokenService, platformUtilsService, async (expired: boolean) => messagingService.send('logout', { expired: expired })); const environmentService = new EnvironmentService(apiService, storageService, null); const userService = new UserService(tokenService, storageService); +const apiKeyService = new ApiKeyService(tokenService, storageService); const containerService = new ContainerService(cryptoService); const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService, - i18nService, platformUtilsService, messagingService, null, logService, false); + i18nService, platformUtilsService, messagingService, null, logService, apiKeyService, false); const configurationService = new ConfigurationService(storageService, secureStorageService); const syncService = new SyncService(configurationService, logService, cryptoFunctionService, apiService, messagingService, i18nService); @@ -130,6 +134,7 @@ export function initFactory(): Function { { provide: PlatformUtilsServiceAbstraction, useValue: platformUtilsService }, { provide: ApiServiceAbstraction, useValue: apiService }, { provide: UserServiceAbstraction, useValue: userService }, + { provide: ApiKeyServiceAbstraction, useValue: apiKeyService }, { provide: MessagingServiceAbstraction, useValue: messagingService }, { provide: BroadcasterService, useValue: broadcasterService }, { provide: StorageServiceAbstraction, useValue: storageService }, diff --git a/src/app/tabs/settings.component.html b/src/app/tabs/settings.component.html index dbf6825c..0131a4a2 100644 --- a/src/app/tabs/settings.component.html +++ b/src/app/tabs/settings.component.html @@ -213,17 +213,6 @@ -
-

{{'account' | 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) {