From 22a1cef49887a4b5c3734f940826b80c4ace0630 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 16 Jul 2020 09:18:25 -0400 Subject: [PATCH] SSO support (#575) * support for sso * resetMasterPassword * update jslib * [Enterprise] Added button to launch portal (#570) * initial commit * Added Enterprise button and used new business portal bool * Reverting services module local changes * Formatted some new lines * Closed alerts on lock (#572) Co-authored-by: Addison Beck * Updated enterprise URL dev (port) (#574) Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Co-authored-by: Addison Beck Co-authored-by: Addison Beck --- src/app/accounts/lock.component.html | 10 +- src/app/accounts/lock.component.ts | 5 +- src/app/accounts/login.component.html | 5 + src/app/accounts/sso.component.html | 31 ++++++ src/app/accounts/sso.component.ts | 120 +++++++++++++++++++++++ src/app/app-routing.module.ts | 6 ++ src/app/app.module.ts | 2 + src/connectors/sso.html | 32 ++++++ src/connectors/sso.scss | 1 + src/connectors/sso.ts | 24 +++++ src/services/htmlStorage.service.ts | 3 +- src/services/webPlatformUtils.service.ts | 6 +- webpack.config.js | 6 ++ 13 files changed, 242 insertions(+), 9 deletions(-) create mode 100644 src/app/accounts/sso.component.html create mode 100644 src/app/accounts/sso.component.ts create mode 100644 src/connectors/sso.html create mode 100644 src/connectors/sso.scss create mode 100644 src/connectors/sso.ts diff --git a/src/app/accounts/lock.component.html b/src/app/accounts/lock.component.html index 24ce14321a..8143838148 100644 --- a/src/app/accounts/lock.component.html +++ b/src/app/accounts/lock.component.html @@ -1,4 +1,4 @@ -
+

@@ -25,9 +25,11 @@


-
+
diff --git a/src/app/accounts/sso.component.html b/src/app/accounts/sso.component.html new file mode 100644 index 0000000000..8d58c24495 --- /dev/null +++ b/src/app/accounts/sso.component.html @@ -0,0 +1,31 @@ + +
+
+ +
+
+ + Logging in, please wait... +
+
+

Quickly log in using your organization's single sign-on portal. Please enter your organization's + identifier to begin.

+
+ + +
+
+
+ + + {{'cancel' | i18n}} + +
+
+
+
+
+
diff --git a/src/app/accounts/sso.component.ts b/src/app/accounts/sso.component.ts new file mode 100644 index 0000000000..cf473258b9 --- /dev/null +++ b/src/app/accounts/sso.component.ts @@ -0,0 +1,120 @@ +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 { 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 { ConstantsService } from 'jslib/services/constants.service'; + +import { Utils } from 'jslib/misc/utils'; + +import { AuthResult } from 'jslib/models/domain/authResult'; + +@Component({ + selector: 'app-sso', + templateUrl: 'sso.component.html', +}) +export class SsoComponent { + identifier: string; + loggingIn = false; + + formPromise: Promise; + onSuccessfulLogin: () => Promise; + onSuccessfulLoginNavigate: () => Promise; + onSuccessfulLoginTwoFactorNavigate: () => Promise; + + protected twoFactorRoute = '2fa'; + protected successRoute = 'lock'; + + private redirectUri = window.location.origin + '/sso-connector.html'; + + constructor(private authService: AuthService, private router: Router, + private i18nService: I18nService, private route: ActivatedRoute, + private storageService: StorageService, private stateService: StateService, + private platformUtilsService: PlatformUtilsService, private apiService: ApiService, + private cryptoFunctionService: CryptoFunctionService, + private passwordGenerationService: PasswordGenerationService) { } + + async ngOnInit() { + const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => { + if (qParams.code != null && qParams.state != null) { + const codeVerifier = await this.storageService.get(ConstantsService.ssoCodeVerifierKey); + const state = await this.storageService.get(ConstantsService.ssoStateKey); + await this.storageService.remove(ConstantsService.ssoCodeVerifierKey); + await this.storageService.remove(ConstantsService.ssoStateKey); + if (qParams.code != null && codeVerifier != null && state != null && state === qParams.state) { + await this.logIn(qParams.code, codeVerifier); + } + } + if (queryParamsSub != null) { + queryParamsSub.unsubscribe(); + } + }); + } + + async submit() { + const passwordOptions: any = { + type: 'password', + length: 64, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + }; + const state = await this.passwordGenerationService.generatePassword(passwordOptions); + const 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); + + const authorizeUrl = this.apiService.identityBaseUrl + '/connect/authorize?' + + 'client_id=web&redirect_uri=' + this.redirectUri + '&' + + 'response_type=code&scope=api offline_access&' + + 'state=' + state + '&code_challenge=' + codeChallenge + '&' + + 'code_challenge_method=S256&response_mode=query&' + + 'domain_hint=' + this.identifier; + this.platformUtilsService.launchUri(authorizeUrl, { sameWindow: true }); + } + + private async logIn(code: string, codeVerifier: string) { + this.loggingIn = true; + try { + this.formPromise = this.authService.logInSso(code, codeVerifier, this.redirectUri); + const response = await this.formPromise; + if (response.twoFactor) { + this.platformUtilsService.eventTrack('SSO Logged In To Two-step'); + if (this.onSuccessfulLoginTwoFactorNavigate != null) { + this.onSuccessfulLoginTwoFactorNavigate(); + } else { + this.router.navigate([this.twoFactorRoute]); + } + } else if (response.resetMasterPassword) { + // TODO: launch reset master password flow + } else { + const disableFavicon = await this.storageService.get(ConstantsService.disableFaviconKey); + await this.stateService.save(ConstantsService.disableFaviconKey, !!disableFavicon); + if (this.onSuccessfulLogin != null) { + this.onSuccessfulLogin(); + } + this.platformUtilsService.eventTrack('SSO Logged In'); + if (this.onSuccessfulLoginNavigate != null) { + this.onSuccessfulLoginNavigate(); + } else { + this.router.navigate([this.successRoute]); + } + } + } catch { } + this.loggingIn = false; + } +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 6150031ca6..b79bb73c59 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -15,6 +15,7 @@ import { LoginComponent } from './accounts/login.component'; import { RecoverDeleteComponent } from './accounts/recover-delete.component'; import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component'; import { RegisterComponent } from './accounts/register.component'; +import { SsoComponent } from './accounts/sso.component'; import { TwoFactorComponent } from './accounts/two-factor.component'; import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component'; import { VerifyRecoverDeleteComponent } from './accounts/verify-recover-delete.component'; @@ -99,6 +100,11 @@ const routes: Routes = [ canActivate: [UnauthGuardService], data: { titleId: 'createAccount' }, }, + { + path: 'sso', component: SsoComponent, + canActivate: [UnauthGuardService], + data: { titleId: 'createAccount' }, // TODO + }, { path: 'hint', component: HintComponent, canActivate: [UnauthGuardService], diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 30e405e727..86e222381a 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -34,6 +34,7 @@ import { LoginComponent } from './accounts/login.component'; import { RecoverDeleteComponent } from './accounts/recover-delete.component'; import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component'; import { RegisterComponent } from './accounts/register.component'; +import { SsoComponent } from './accounts/sso.component'; import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; import { TwoFactorComponent } from './accounts/two-factor.component'; import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component'; @@ -349,6 +350,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); SelectCopyDirective, SettingsComponent, ShareComponent, + SsoComponent, StopClickDirective, StopPropDirective, TaxInfoComponent, diff --git a/src/connectors/sso.html b/src/connectors/sso.html new file mode 100644 index 0000000000..6fd51a2b93 --- /dev/null +++ b/src/connectors/sso.html @@ -0,0 +1,32 @@ + + + + + + + + + Logging into Bitwarden... + + + + + + + + + +
+
+ +

+ +

+

+ Logging into Bitwarden... +

+
+
+ + + diff --git a/src/connectors/sso.scss b/src/connectors/sso.scss new file mode 100644 index 0000000000..a4c7f9b25b --- /dev/null +++ b/src/connectors/sso.scss @@ -0,0 +1 @@ +@import "../scss/styles.scss"; diff --git a/src/connectors/sso.ts b/src/connectors/sso.ts new file mode 100644 index 0000000000..b5a172a97e --- /dev/null +++ b/src/connectors/sso.ts @@ -0,0 +1,24 @@ +// tslint:disable-next-line +require('./sso.scss'); + +document.addEventListener('DOMContentLoaded', (event) => { + const code = getQsParam('code'); + const state = getQsParam('state'); + window.location.href = window.location.origin + '/#/sso?code=' + code + '&state=' + state; +}); + +function getQsParam(name: string) { + const url = window.location.href; + name = name.replace(/[\[\]]/g, '\\$&'); + const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'); + const results = regex.exec(url); + + if (!results) { + return null; + } + if (!results[2]) { + return ''; + } + + return decodeURIComponent(results[2].replace(/\+/g, ' ')); +} diff --git a/src/services/htmlStorage.service.ts b/src/services/htmlStorage.service.ts index 88870c84fb..96d778dab9 100644 --- a/src/services/htmlStorage.service.ts +++ b/src/services/htmlStorage.service.ts @@ -6,7 +6,8 @@ export class HtmlStorageService implements StorageService { private localStorageKeys = new Set(['appId', 'anonymousAppId', 'rememberedEmail', 'passwordGenerationOptions', ConstantsService.disableFaviconKey, 'rememberEmail', 'enableGravatars', 'enableFullWidth', ConstantsService.localeKey, ConstantsService.autoConfirmFingerprints, - ConstantsService.vaultTimeoutKey, ConstantsService.vaultTimeoutActionKey]); + ConstantsService.vaultTimeoutKey, ConstantsService.vaultTimeoutActionKey, ConstantsService.ssoCodeVerifierKey, + ConstantsService.ssoStateKey]); private localStorageStartsWithKeys = ['twoFactorToken_', ConstantsService.collapsedGroupingsKey + '_']; constructor(private platformUtilsService: PlatformUtilsService) { } diff --git a/src/services/webPlatformUtils.service.ts b/src/services/webPlatformUtils.service.ts index e6b18b6166..1efe660d6d 100644 --- a/src/services/webPlatformUtils.service.ts +++ b/src/services/webPlatformUtils.service.ts @@ -93,8 +93,10 @@ export class WebPlatformUtilsService implements PlatformUtilsService { launchUri(uri: string, options?: any): void { const a = document.createElement('a'); a.href = uri; - a.target = '_blank'; - a.rel = 'noreferrer noopener'; + if (options == null || !options.sameWindow) { + a.target = '_blank'; + a.rel = 'noreferrer noopener'; + } a.classList.add('d-none'); document.body.appendChild(a); a.click(); diff --git a/webpack.config.js b/webpack.config.js index 3391d5ff8d..5f00785a89 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -88,6 +88,11 @@ const plugins = [ filename: 'u2f-connector.html', chunks: ['connectors/u2f'], }), + new HtmlWebpackPlugin({ + template: './src/connectors/sso.html', + filename: 'sso-connector.html', + chunks: ['connectors/sso'], + }), new CopyWebpackPlugin([ { from: './src/.nojekyll' }, { from: './src/manifest.json' }, @@ -152,6 +157,7 @@ const config = { 'app/main': './src/app/main.ts', 'connectors/u2f': './src/connectors/u2f.js', 'connectors/duo': './src/connectors/duo.ts', + 'connectors/sso': './src/connectors/sso.ts', }, externals: { 'u2f': 'u2f',