diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 64a62017bb..ea0387fc6f 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -19,6 +19,9 @@ "login": { "message": "Log In" }, + "enterpriseSingleSignOn": { + "message": "Enterprise Single Sign-On" + }, "cancel": { "message": "Cancel" }, @@ -1297,4 +1300,4 @@ "vaultTimeoutLogOutConfirmationTitle": { "message": "Timeout Action Confirmation" } -} +} \ No newline at end of file diff --git a/src/background/main.background.ts b/src/background/main.background.ts index 885b94b8b2..bbd455a52c 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -4,6 +4,7 @@ import { ApiService, AppIdService, AuditService, + AuthService, CipherService, CollectionService, ConstantsService, @@ -13,6 +14,7 @@ import { FolderService, PasswordGenerationService, SettingsService, + StateService, SyncService, TokenService, TotpService, @@ -31,6 +33,7 @@ import { ApiService as ApiServiceAbstraction, AppIdService as AppIdServiceAbstraction, AuditService as AuditServiceAbstraction, + AuthService as AuthServiceAbstraction, CipherService as CipherServiceAbstraction, CollectionService as CollectionServiceAbstraction, CryptoService as CryptoServiceAbstraction, @@ -41,6 +44,7 @@ import { PasswordGenerationService as PasswordGenerationServiceAbstraction, PlatformUtilsService as PlatformUtilsServiceAbstraction, SettingsService as SettingsServiceAbstraction, + StateService as StateServiceAbstraction, StorageService as StorageServiceAbstraction, SyncService as SyncServiceAbstraction, TokenService as TokenServiceAbstraction, @@ -74,6 +78,7 @@ import BrowserMessagingService from '../services/browserMessaging.service'; import BrowserPlatformUtilsService from '../services/browserPlatformUtils.service'; import BrowserStorageService from '../services/browserStorage.service'; import I18nService from '../services/i18n.service'; +import { PopupUtilsService } from '../popup/services/popup-utils.service'; import { AutofillService as AutofillServiceAbstraction } from '../services/abstractions/autofill.service'; @@ -101,13 +106,16 @@ export default class MainBackground { autofillService: AutofillServiceAbstraction; containerService: ContainerService; auditService: AuditServiceAbstraction; + authService: AuthServiceAbstraction; exportService: ExportServiceAbstraction; searchService: SearchServiceAbstraction; notificationsService: NotificationsServiceAbstraction; + stateService: StateServiceAbstraction; systemService: SystemServiceAbstraction; eventService: EventServiceAbstraction; policyService: PolicyServiceAbstraction; analytics: Analytics; + popupUtilsService: PopupUtilsService; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -147,6 +155,9 @@ export default class MainBackground { this.apiService = new ApiService(this.tokenService, this.platformUtilsService, (expired: boolean) => this.logout(expired)); this.userService = new UserService(this.tokenService, this.storageService); + this.authService = new AuthService(this.cryptoService, this.apiService, this.userService, + this.tokenService, this.appIdService, this.i18nService, this.platformUtilsService, + this.messagingService, this.vaultTimeoutService, null); this.settingsService = new SettingsService(this.userService, this.storageService); this.cipherService = new CipherService(this.cryptoService, this.userService, this.settingsService, this.apiService, this.storageService, this.i18nService, () => this.searchService); @@ -155,6 +166,7 @@ export default class MainBackground { this.collectionService = new CollectionService(this.cryptoService, this.userService, this.storageService, this.i18nService); this.searchService = new SearchService(this.cipherService, this.platformUtilsService); + this.stateService = new StateService(); this.policyService = new PolicyService(this.userService, this.storageService); this.vaultTimeoutService = new VaultTimeoutService(this.cipherService, this.folderService, this.collectionService, this.cryptoService, this.platformUtilsService, this.storageService, @@ -190,6 +202,7 @@ export default class MainBackground { this.notificationsService); this.analytics = new Analytics(window, () => BrowserApi.gaFilter(), this.platformUtilsService, this.storageService, this.appIdService); + this.popupUtilsService = new PopupUtilsService(this.platformUtilsService); this.systemService = new SystemService(this.storageService, this.vaultTimeoutService, this.messagingService, this.platformUtilsService, () => { const forceWindowReload = this.platformUtilsService.isSafari() || @@ -206,7 +219,8 @@ export default class MainBackground { // Background this.runtimeBackground = new RuntimeBackground(this, this.autofillService, this.cipherService, this.platformUtilsService as BrowserPlatformUtilsService, this.storageService, this.i18nService, - this.analytics, this.notificationsService, this.systemService, this.vaultTimeoutService); + this.analytics, this.notificationsService, this.systemService, this.vaultTimeoutService, this.syncService, + this.authService, this.stateService, this.environmentService, this.popupUtilsService); this.commandsBackground = new CommandsBackground(this, this.passwordGenerationService, this.platformUtilsService, this.analytics, this.vaultTimeoutService); diff --git a/src/background/runtime.background.ts b/src/background/runtime.background.ts index 6aad2decfa..e74d8b44fa 100644 --- a/src/background/runtime.background.ts +++ b/src/background/runtime.background.ts @@ -4,14 +4,18 @@ import { CipherView } from 'jslib/models/view/cipherView'; import { LoginUriView } from 'jslib/models/view/loginUriView'; import { LoginView } from 'jslib/models/view/loginView'; -import { ConstantsService } from 'jslib/services/constants.service'; - -import { I18nService } from 'jslib/abstractions/i18n.service'; - -import { Analytics } from 'jslib/misc'; - +import { AuthService } from 'jslib/abstractions/auth.service'; +import { AutofillService } from '../services/abstractions/autofill.service'; +import BrowserPlatformUtilsService from '../services/browserPlatformUtils.service'; import { CipherService } from 'jslib/abstractions/cipher.service'; +import { ConstantsService } from 'jslib/services/constants.service'; +import { EnvironmentService } from 'jslib/abstractions/environment.service'; +import { I18nService } from 'jslib/abstractions/i18n.service'; +import { NotificationsService } from 'jslib/abstractions/notifications.service'; +import { PopupUtilsService } from '../popup/services/popup-utils.service'; +import { StateService } from 'jslib/abstractions/state.service'; import { StorageService } from 'jslib/abstractions/storage.service'; +import { SyncService } from 'jslib/abstractions/sync.service'; import { SystemService } from 'jslib/abstractions/system.service'; import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service'; @@ -19,11 +23,7 @@ import { BrowserApi } from '../browser/browserApi'; import MainBackground from './main.background'; -import { AutofillService } from '../services/abstractions/autofill.service'; -import BrowserPlatformUtilsService from '../services/browserPlatformUtils.service'; - -import { NotificationsService } from 'jslib/abstractions/notifications.service'; - +import { Analytics } from 'jslib/misc'; import { Utils } from 'jslib/misc/utils'; export default class RuntimeBackground { @@ -37,7 +37,9 @@ export default class RuntimeBackground { private cipherService: CipherService, private platformUtilsService: BrowserPlatformUtilsService, private storageService: StorageService, private i18nService: I18nService, private analytics: Analytics, private notificationsService: NotificationsService, - private systemService: SystemService, private vaultTimeoutService: VaultTimeoutService) { + private systemService: SystemService, private vaultTimeoutService: VaultTimeoutService, + private syncService: SyncService, private authService: AuthService, private stateService: StateService, + private environmentService: EnvironmentService, private popupUtilsService : PopupUtilsService) { this.isSafari = this.platformUtilsService.isSafari(); this.runtime = this.isSafari ? {} : chrome.runtime; @@ -162,6 +164,27 @@ export default class RuntimeBackground { break; } break; + case 'authResult': + var vaultUrl = this.environmentService.webVaultUrl; + if(!vaultUrl) { + vaultUrl = 'https://vault.bitwarden.com'; + } + + if(!msg.referrer) { + return; + } + + if(!vaultUrl.includes(msg.referrer)) { + return; + } + + try { + chrome.tabs.create({ + url: 'popup/index.html?uilocation=popout#/sso?code=' + msg.code + '&state=' + msg.state + }); + } + catch { } + break; default: break; } diff --git a/src/browser/browserApi.ts b/src/browser/browserApi.ts index dedcdaac10..a034a52bba 100644 --- a/src/browser/browserApi.ts +++ b/src/browser/browserApi.ts @@ -213,4 +213,15 @@ export class BrowserApi { return chrome.runtime.reload(); } } + + static reloadOpenWindows() { + if(!BrowserApi.isSafariApi) + { + var sidebarName : string = 'sidebar'; + var sidebarWindows = chrome.extension.getViews({ type: sidebarName }); + if(sidebarWindows && sidebarWindows.length > 0) { + sidebarWindows[0].location.reload(); + } + } + } } diff --git a/src/content/sso.ts b/src/content/sso.ts new file mode 100644 index 0000000000..cc936d91ab --- /dev/null +++ b/src/content/sso.ts @@ -0,0 +1,13 @@ +window.addEventListener("message", function(event) { + if (event.source != window) + return; + + if (event.data.command && (event.data.command == "authResult")) { + chrome.runtime.sendMessage({ + command: event.data.command, + code: event.data.code, + state: event.data.state, + referrer: event.source.location.hostname + }); + } +}, false) \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index 2f25aa0654..e3ccb6417a 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -41,6 +41,18 @@ ], "run_at": "document_start" }, + { + "all_frames": false, + "js": [ + "content/sso.js" + ], + "matches": [ + "http://*/*", + "https://*/*", + "file:///*" + ], + "run_at": "document_start" + }, { "all_frames": true, "css": [ diff --git a/src/popup/accounts/home.component.html b/src/popup/accounts/home.component.html index a4e1b3393b..3cdfa2e99f 100644 --- a/src/popup/accounts/home.component.html +++ b/src/popup/accounts/home.component.html @@ -3,6 +3,9 @@

{{'loginOrCreateNewAccount' | i18n}}

{{'login' | i18n}} + + {{'enterpriseSingleSignOn' | i18n}} + {{'createAccount' | i18n}} diff --git a/src/popup/accounts/home.component.ts b/src/popup/accounts/home.component.ts index 2780e2b5bc..49ec71d8a8 100644 --- a/src/popup/accounts/home.component.ts +++ b/src/popup/accounts/home.component.ts @@ -1,7 +1,55 @@ import { Component } from '@angular/core'; +import { ConstantsService } from 'jslib/services/constants.service' +import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service'; +import { EnvironmentService } from 'jslib/abstractions/environment.service'; +import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; +import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service'; +import { StorageService } from 'jslib/abstractions/storage.service'; + +import { Utils } from 'jslib/misc/utils'; + @Component({ selector: 'app-home', templateUrl: 'home.component.html', }) -export class HomeComponent { } +export class HomeComponent { + constructor( + protected platformUtilsService: PlatformUtilsService, + private passwordGenerationService : PasswordGenerationService, + private cryptoFunctionService: CryptoFunctionService, + private environmentService: EnvironmentService, + private storageService : StorageService) { } + + async launchSsoBrowser() { + // Generate necessary sso params + const passwordOptions: any = { + type: 'password', + length: 64, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + }; + + const state = (await this.passwordGenerationService.generatePassword(passwordOptions)) + ':clientId=browser'; + let codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, 'sha256'); + const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + + await this.storageService.save(ConstantsService.ssoCodeVerifierKey, codeVerifier); + await this.storageService.save(ConstantsService.ssoStateKey, state); + + let url = this.environmentService.getWebVaultUrl(); + if (url == null) { + url = 'https://vault.bitwarden.com'; + } + + const redirectUri = url + '/sso-connector.html'; + + // Launch browser + this.platformUtilsService.launchUri(url + '/#/sso?clientId=browser' + + '&redirectUri=' + encodeURIComponent(redirectUri) + + '&state=' + state + '&codeChallenge=' + codeChallenge); + } +} diff --git a/src/popup/accounts/login.component.ts b/src/popup/accounts/login.component.ts index 0ff3c52246..66873e538c 100644 --- a/src/popup/accounts/login.component.ts +++ b/src/popup/accounts/login.component.ts @@ -19,13 +19,12 @@ import { LoginComponent as BaseLoginComponent } from 'jslib/angular/components/l }) export class LoginComponent extends BaseLoginComponent { constructor(authService: AuthService, router: Router, - platformUtilsService: PlatformUtilsService, i18nService: I18nService, - syncService: SyncService, storageService: StorageService, - stateService: StateService, environmentService: EnvironmentService, - passwordGenerationService: PasswordGenerationService, - cryptoFunctionService: CryptoFunctionService) { - super(authService, router, platformUtilsService, i18nService, stateService, environmentService, - passwordGenerationService, cryptoFunctionService, storageService); + protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, + protected stateService: StateService, protected environmentService: EnvironmentService, + protected passwordGenerationService: PasswordGenerationService, + protected cryptoFunctionService: CryptoFunctionService, + storageService: StorageService, syncService : SyncService) { + super(authService, router, platformUtilsService, i18nService, stateService, environmentService, passwordGenerationService, cryptoFunctionService, storageService); super.onSuccessfulLogin = () => { return syncService.fullSync(true); }; diff --git a/src/popup/accounts/sso.component.html b/src/popup/accounts/sso.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/popup/accounts/sso.component.ts b/src/popup/accounts/sso.component.ts new file mode 100644 index 0000000000..e4f9a07c84 --- /dev/null +++ b/src/popup/accounts/sso.component.ts @@ -0,0 +1,51 @@ +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 { 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 { SyncService } from 'jslib/abstractions/sync.service'; + +import { SsoComponent as BaseSsoComponent } from 'jslib/angular/components/sso.component'; +import { BrowserApi } from '../../browser/browserApi'; + +@Component({ + selector: 'app-sso', + templateUrl: 'sso.component.html', +}) +export class SsoComponent extends BaseSsoComponent { + constructor(authService: AuthService, router: Router, + i18nService: I18nService, route: ActivatedRoute, + storageService: StorageService, stateService: StateService, + platformUtilsService: PlatformUtilsService, apiService: ApiService, + cryptoFunctionService: CryptoFunctionService, passwordGenerationService: PasswordGenerationService, + syncService: SyncService, private environmentService: EnvironmentService ) { + super(authService, router, i18nService, route, storageService, stateService, platformUtilsService, + apiService, cryptoFunctionService, passwordGenerationService); + + let url = this.environmentService.getWebVaultUrl(); + if (url == null) { + url = 'https://vault.bitwarden.com'; + } + + this.redirectUri = url + '/sso-connector.html'; + this.clientId = 'browser'; + + super.onSuccessfulLogin = () => { + BrowserApi.reloadOpenWindows(); + const thisWindow = window.open('', '_self'); + thisWindow.close(); + return syncService.fullSync(true); + }; + } +} diff --git a/src/popup/app-routing.module.ts b/src/popup/app-routing.module.ts index 5f419c5400..ca3c4b37b8 100644 --- a/src/popup/app-routing.module.ts +++ b/src/popup/app-routing.module.ts @@ -18,6 +18,7 @@ import { LoginComponent } from './accounts/login.component'; import { RegisterComponent } from './accounts/register.component'; import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; import { TwoFactorComponent } from './accounts/two-factor.component'; +import { SsoComponent } from './accounts/sso.component'; import { PasswordGeneratorHistoryComponent } from './generator/password-generator-history.component'; import { PasswordGeneratorComponent } from './generator/password-generator.component'; import { PrivateModeComponent } from './private-mode.component'; @@ -79,6 +80,12 @@ const routes: Routes = [ canActivate: [LaunchGuardService], data: { state: '2fa-options' }, }, + { + path: 'sso', + component: SsoComponent, + canActivate: [LaunchGuardService], + data: { state: 'sso' }, + }, { path: 'register', component: RegisterComponent, diff --git a/src/popup/app.module.ts b/src/popup/app.module.ts index 0a5c20939b..7fce0b27d7 100644 --- a/src/popup/app.module.ts +++ b/src/popup/app.module.ts @@ -23,6 +23,7 @@ import { LoginComponent } from './accounts/login.component'; import { RegisterComponent } from './accounts/register.component'; import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; import { TwoFactorComponent } from './accounts/two-factor.component'; +import { SsoComponent } from './accounts/sso.component'; import { AppComponent } from './app.component'; import { PasswordGeneratorHistoryComponent } from './generator/password-generator-history.component'; import { PasswordGeneratorComponent } from './generator/password-generator.component'; @@ -206,6 +207,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); TrueFalseValueDirective, TwoFactorOptionsComponent, TwoFactorComponent, + SsoComponent, ViewComponent, ], entryComponents: [], diff --git a/src/popup/services/services.module.ts b/src/popup/services/services.module.ts index 5cf62e8501..785aba0675 100644 --- a/src/popup/services/services.module.ts +++ b/src/popup/services/services.module.ts @@ -41,6 +41,7 @@ import { TokenService } from 'jslib/abstractions/token.service'; import { TotpService } from 'jslib/abstractions/totp.service'; import { UserService } from 'jslib/abstractions/user.service'; import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service'; +import { WebCryptoFunctionService } from 'jslib/services/webCryptoFunction.service'; import { AutofillService } from '../../services/abstractions/autofill.service'; import BrowserMessagingService from '../../services/browserMessaging.service'; @@ -68,9 +69,11 @@ export const authService = new AuthService(getBgService('cryptoSe getBgService('apiService')(), getBgService('userService')(), getBgService('tokenService')(), getBgService('appIdService')(), getBgService('i18nService')(), getBgService('platformUtilsService')(), - messagingService, getBgService('vaultTimeoutService')()); + messagingService, getBgService('vaultTimeoutService')(), null); export const searchService = new PopupSearchService(getBgService('searchService')(), getBgService('cipherService')(), getBgService('platformUtilsService')()); +export const cryptoFunctionService: CryptoFunctionService = new WebCryptoFunctionService(window, + getBgService('platformUtilsService')()); export function initFactory(i18nService: I18nService, storageService: StorageService, popupUtilsService: PopupUtilsService): Function { @@ -122,6 +125,7 @@ export function initFactory(i18nService: I18nService, storageService: StorageSer { provide: AuthServiceAbstraction, useValue: authService }, { provide: StateServiceAbstraction, useValue: stateService }, { provide: SearchServiceAbstraction, useValue: searchService }, + { provide: CryptoFunctionService, useValue: cryptoFunctionService }, { provide: AuditService, useFactory: getBgService('auditService'), deps: [] }, { provide: CipherService, useFactory: getBgService('cipherService'), deps: [] }, { provide: FolderService, useFactory: getBgService('folderService'), deps: [] }, diff --git a/src/safari/safari/Info.plist b/src/safari/safari/Info.plist index 58d3382c82..7676d60e40 100644 --- a/src/safari/safari/Info.plist +++ b/src/safari/safari/Info.plist @@ -53,6 +53,10 @@ Script app/content/shortcuts.js + + Script + app/content/sso.js + SFSafariToolbarItem diff --git a/src/services/browserPlatformUtils.service.ts b/src/services/browserPlatformUtils.service.ts index da8239b433..13c6516c6f 100644 --- a/src/services/browserPlatformUtils.service.ts +++ b/src/services/browserPlatformUtils.service.ts @@ -295,7 +295,7 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService return Promise.resolve(false); } - private sidebarViewName(): string { + sidebarViewName(): string { if ((window as any).chrome.sidebarAction && this.isFirefox()) { return 'sidebar'; } else if (this.isOpera() && (typeof opr !== 'undefined') && opr.sidebarAction) { diff --git a/webpack.config.js b/webpack.config.js index a93e8c9ba2..66dd1c423a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -136,6 +136,7 @@ const config = { 'content/autofiller': './src/content/autofiller.ts', 'content/notificationBar': './src/content/notificationBar.ts', 'content/shortcuts': './src/content/shortcuts.ts', + 'content/sso': './src/content/sso.ts', 'notification/bar': './src/notification/bar.js', }, optimization: {