From caea4775b34d1b54790d68aa972413c8405e0782 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 13 Aug 2020 14:32:07 -0400 Subject: [PATCH] SSO feature (#604) * Update feature/sso jslib 261a200 -> 2e823ea (#589) * [SSO] Reset master password (#580) * Initial commit reset master password (sso) * Reverted order of two factor/reset password conditional * Added necessary resetMasterPassword flag for potential entry into RMP flow * Complete Revamp: Reverted Register // Deleted reset-master-password // updated sso/(settings)change password to use use super class // Adjust routing/messages // Created (accounts) change-password * Updated button -> Set Master Password * Refactored change password sub classes to use new submit pattern * Cleaned import statements * Update jslib (7fa5178 -> fe167be) * Update jslib fe167be - >34632e5 * Fixed sso base class import * merge master * Fixed missing semicolon // updated jslib to whats in feature/sso * Fixed two factor formatting * Added new change password component to app module * Updated component selector * updating jslib 34632e5 -> 2e823ea * Fixed lint warning in two-factor component Co-authored-by: Kyle Spearrin * Update jslib to 101c568 (#594) * Support for dynamic clientid (#595) * support third party sso clients * jslib update * update jslib * Update change-password.component.ts * Update sso.component.ts * Update app.module.ts Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> --- .../accounts/change-password.component.html | 45 ++++ src/app/accounts/change-password.component.ts | 65 ++++++ src/app/accounts/sso.component.ts | 110 ++-------- src/app/accounts/two-factor.component.ts | 26 ++- src/app/app-routing.module.ts | 6 + src/app/app.module.ts | 2 + src/app/settings/change-password.component.ts | 194 +++++------------- src/locales/en/messages.json | 6 + 8 files changed, 212 insertions(+), 242 deletions(-) create mode 100644 src/app/accounts/change-password.component.html create mode 100644 src/app/accounts/change-password.component.ts diff --git a/src/app/accounts/change-password.component.html b/src/app/accounts/change-password.component.html new file mode 100644 index 0000000000..460120a49d --- /dev/null +++ b/src/app/accounts/change-password.component.html @@ -0,0 +1,45 @@ +
+

{{'setMasterPassword' | i18n}}

+
+{{'ssoCompleteRegistration' | i18n}} + + {{'masterPasswordPolicyInEffect' | i18n}} +
    +
  • + {{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}} +
  • +
  • + {{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}} +
  • +
  • {{'policyInEffectUppercase' | i18n}}
  • +
  • {{'policyInEffectLowercase' | i18n}}
  • +
  • {{'policyInEffectNumbers' | i18n}}
  • +
  • {{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}
  • +
+
+ +
+
+
+
+ + + +
+
+
+
+ + +
+
+
+ +
diff --git a/src/app/accounts/change-password.component.ts b/src/app/accounts/change-password.component.ts new file mode 100644 index 0000000000..84e55c6998 --- /dev/null +++ b/src/app/accounts/change-password.component.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; + +import { ApiService } from 'jslib/abstractions/api.service'; +import { CipherService } from 'jslib/abstractions/cipher.service'; +import { CryptoService } from 'jslib/abstractions/crypto.service'; +import { FolderService } from 'jslib/abstractions/folder.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 { PolicyService } from 'jslib/abstractions/policy.service'; +import { SyncService } from 'jslib/abstractions/sync.service'; +import { UserService } from 'jslib/abstractions/user.service'; + +import { CipherString } from 'jslib/models/domain/cipherString'; +import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey'; + +import { SetPasswordRequest } from 'jslib/models/request/setPasswordRequest'; + +import { + ChangePasswordComponent as BaseChangePasswordComponent, +} from 'jslib/angular/components/change-password.component'; + +@Component({ + selector: 'app-accounts-change-password', + templateUrl: 'change-password.component.html', +}) +export class ChangePasswordComponent extends BaseChangePasswordComponent { + onSuccessfulChangePassword: () => Promise; + successRoute = 'lock'; + + constructor(apiService: ApiService, i18nService: I18nService, + cryptoService: CryptoService, messagingService: MessagingService, + userService: UserService, passwordGenerationService: PasswordGenerationService, + platformUtilsService: PlatformUtilsService, folderService: FolderService, + cipherService: CipherService, syncService: SyncService, + policyService: PolicyService, router: Router, private route: ActivatedRoute) { + super(apiService, i18nService, cryptoService, messagingService, userService, passwordGenerationService, + platformUtilsService, folderService, cipherService, syncService, policyService, router); + } + + async performSubmitActions(newMasterPasswordHash: string, newKey: SymmetricCryptoKey, + newEncKey: [SymmetricCryptoKey, CipherString]) { + const setRequest = new SetPasswordRequest(); + setRequest.newMasterPasswordHash = newMasterPasswordHash; + setRequest.key = newEncKey[1].encryptedString; + + try { + this.formPromise = this.apiService.setPassword(setRequest); + await this.formPromise; + + if (this.onSuccessfulChangePassword != null) { + this.onSuccessfulChangePassword(); + } else { + this.router.navigate([this.successRoute]); + } + } catch { + this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); + } + } +} diff --git a/src/app/accounts/sso.component.ts b/src/app/accounts/sso.component.ts index cf473258b9..f906d4f95e 100644 --- a/src/app/accounts/sso.component.ts +++ b/src/app/accounts/sso.component.ts @@ -13,108 +13,22 @@ 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'; +import { SsoComponent as BaseSsoComponent } from 'jslib/angular/components/sso.component'; @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; +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) { + super(authService, router, i18nService, route, storageService, stateService, platformUtilsService, + apiService, cryptoFunctionService, passwordGenerationService); + this.redirectUri = window.location.origin + '/sso-connector.html'; + this.clientId = 'web'; } } diff --git a/src/app/accounts/two-factor.component.ts b/src/app/accounts/two-factor.component.ts index 87d4e43f41..7cae81f388 100644 --- a/src/app/accounts/two-factor.component.ts +++ b/src/app/accounts/two-factor.component.ts @@ -5,7 +5,10 @@ import { ViewContainerRef, } from '@angular/core'; -import { Router } from '@angular/router'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { TwoFactorOptionsComponent } from './two-factor-options.component'; @@ -34,12 +37,25 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { i18nService: I18nService, apiService: ApiService, platformUtilsService: PlatformUtilsService, stateService: StateService, environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver, - storageService: StorageService) { + storageService: StorageService, private route: ActivatedRoute) { super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService, stateService, storageService); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } + async ngOnInit() { + const queryParamsSub = this.route.queryParams.subscribe((qParams) => { + if (qParams.resetMasterPassword != null) { + this.resetMasterPassword = qParams.resetMasterPassword; + } + + if (queryParamsSub != null) { + queryParamsSub.unsubscribe(); + } + }); + super.ngOnInit(); + } + anotherMethod() { const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent); const modal = this.twoFactorOptionsModal.createComponent(factory).instance; @@ -66,7 +82,11 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams }); await this.stateService.remove('loginRedirect'); } else { - this.router.navigate([this.successRoute]); + this.router.navigate([this.successRoute], { + queryParams: { + resetMasterPassword: this.resetMasterPassword, + }, + }); } } } diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index b79bb73c59..9053805b52 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -9,6 +9,7 @@ import { OrganizationLayoutComponent } from './layouts/organization-layout.compo import { UserLayoutComponent } from './layouts/user-layout.component'; import { AcceptOrganizationComponent } from './accounts/accept-organization.component'; +import { ChangePasswordComponent } from './accounts/change-password.component'; import { HintComponent } from './accounts/hint.component'; import { LockComponent } from './accounts/lock.component'; import { LoginComponent } from './accounts/login.component'; @@ -105,6 +106,11 @@ const routes: Routes = [ canActivate: [UnauthGuardService], data: { titleId: 'createAccount' }, // TODO }, + { + path: 'change-password', component: ChangePasswordComponent, + canActivate: [UnauthGuardService], + data: { titleId: 'setMasterPassword' }, + }, { path: 'hint', component: HintComponent, canActivate: [UnauthGuardService], diff --git a/src/app/app.module.ts b/src/app/app.module.ts index b762782391..7ea8c2f3d9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -28,6 +28,7 @@ import { OrganizationLayoutComponent } from './layouts/organization-layout.compo import { UserLayoutComponent } from './layouts/user-layout.component'; import { AcceptOrganizationComponent } from './accounts/accept-organization.component'; +import { ChangePasswordComponent as AccountsChangePasswordComponent } from './accounts/change-password.component'; import { HintComponent } from './accounts/hint.component'; import { LockComponent } from './accounts/lock.component'; import { LoginComponent } from './accounts/login.component'; @@ -248,6 +249,7 @@ registerLocaleData(localeZhTw, 'zh-TW'); A11yTitleDirective, AcceptOrganizationComponent, AccountComponent, + AccountsChangePasswordComponent, AddCreditComponent, AddEditComponent, AdjustPaymentComponent, diff --git a/src/app/settings/change-password.component.ts b/src/app/settings/change-password.component.ts index 079877a652..5997481282 100644 --- a/src/app/settings/change-password.component.ts +++ b/src/app/settings/change-password.component.ts @@ -1,10 +1,6 @@ -import { - Component, - OnInit, -} from '@angular/core'; +import { Component } from '@angular/core'; -import { ToasterService } from 'angular2-toaster'; -import { Angulartics2 } from 'angulartics2'; +import { Router } from '@angular/router'; import { ApiService } from 'jslib/abstractions/api.service'; import { CipherService } from 'jslib/abstractions/cipher.service'; @@ -18,8 +14,11 @@ import { PolicyService } from 'jslib/abstractions/policy.service'; import { SyncService } from 'jslib/abstractions/sync.service'; import { UserService } from 'jslib/abstractions/user.service'; +import { + ChangePasswordComponent as BaseChangePasswordComponent, +} from 'jslib/angular/components/change-password.component'; + import { CipherString } from 'jslib/models/domain/cipherString'; -import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordPolicyOptions'; import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey'; import { CipherWithIdRequest } from 'jslib/models/request/cipherWithIdRequest'; @@ -31,136 +30,18 @@ import { UpdateKeyRequest } from 'jslib/models/request/updateKeyRequest'; selector: 'app-change-password', templateUrl: 'change-password.component.html', }) -export class ChangePasswordComponent implements OnInit { - currentMasterPassword: string; - newMasterPassword: string; - confirmNewMasterPassword: string; - formPromise: Promise; - masterPasswordScore: number; +export class ChangePasswordComponent extends BaseChangePasswordComponent { rotateEncKey = false; - enforcedPolicyOptions: MasterPasswordPolicyOptions; + currentMasterPassword: string; - private masterPasswordStrengthTimeout: any; - private email: string; - - constructor(private apiService: ApiService, private i18nService: I18nService, - private analytics: Angulartics2, private toasterService: ToasterService, - private cryptoService: CryptoService, private messagingService: MessagingService, - private userService: UserService, private passwordGenerationService: PasswordGenerationService, - private platformUtilsService: PlatformUtilsService, private folderService: FolderService, - private cipherService: CipherService, private syncService: SyncService, - private policyService: PolicyService) { } - - async ngOnInit() { - this.email = await this.userService.getEmail(); - this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(); - } - - getPasswordScoreAlertDisplay() { - if (this.enforcedPolicyOptions == null) { - return ''; - } - - let str: string; - switch (this.enforcedPolicyOptions.minComplexity) { - case 4: - str = this.i18nService.t('strong'); - break; - case 3: - str = this.i18nService.t('good'); - break; - default: - str = this.i18nService.t('weak'); - break; - } - return str + ' (' + this.enforcedPolicyOptions.minComplexity + ')'; - } - - async submit() { - const hasEncKey = await this.cryptoService.hasEncKey(); - if (!hasEncKey) { - this.toasterService.popAsync('error', null, this.i18nService.t('updateKey')); - return; - } - - if (this.currentMasterPassword == null || this.currentMasterPassword === '' || - this.newMasterPassword == null || this.newMasterPassword === '') { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassRequired')); - return; - } - if (this.newMasterPassword.length < 8) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassLength')); - return; - } - if (this.newMasterPassword !== this.confirmNewMasterPassword) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPassDoesntMatch')); - return; - } - - const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword, - this.getPasswordStrengthUserInput()); - - if (this.enforcedPolicyOptions != null && - !this.policyService.evaluateMasterPassword( - strengthResult.score, - this.newMasterPassword, - this.enforcedPolicyOptions)) { - this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), - this.i18nService.t('masterPasswordPolicyRequirementsNotMet')); - return; - } - - if (strengthResult != null && strengthResult.score < 3) { - const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'), - this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'), - 'warning'); - if (!result) { - return; - } - } - - if (this.rotateEncKey) { - await this.syncService.fullSync(true); - } - - const request = new PasswordRequest(); - request.masterPasswordHash = await this.cryptoService.hashPassword(this.currentMasterPassword, null); - const email = await this.userService.getEmail(); - const kdf = await this.userService.getKdf(); - const kdfIterations = await this.userService.getKdfIterations(); - const newKey = await this.cryptoService.makeKey(this.newMasterPassword, email.trim().toLowerCase(), - kdf, kdfIterations); - request.newMasterPasswordHash = await this.cryptoService.hashPassword(this.newMasterPassword, newKey); - const newEncKey = await this.cryptoService.remakeEncKey(newKey); - request.key = newEncKey[1].encryptedString; - try { - if (this.rotateEncKey) { - this.formPromise = this.apiService.postPassword(request).then(() => { - return this.updateKey(newKey, request.newMasterPasswordHash); - }); - } else { - this.formPromise = this.apiService.postPassword(request); - } - await this.formPromise; - this.analytics.eventTrack.next({ action: 'Changed Password' }); - this.toasterService.popAsync('success', this.i18nService.t('masterPasswordChanged'), - this.i18nService.t('logBackIn')); - this.messagingService.send('logout'); - } catch { } - } - - updatePasswordStrength() { - if (this.masterPasswordStrengthTimeout != null) { - clearTimeout(this.masterPasswordStrengthTimeout); - } - this.masterPasswordStrengthTimeout = setTimeout(() => { - const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword, - this.getPasswordStrengthUserInput()); - this.masterPasswordScore = strengthResult == null ? null : strengthResult.score; - }, 300); + constructor(apiService: ApiService, i18nService: I18nService, + cryptoService: CryptoService, messagingService: MessagingService, + userService: UserService, passwordGenerationService: PasswordGenerationService, + platformUtilsService: PlatformUtilsService, folderService: FolderService, + cipherService: CipherService, syncService: SyncService, + policyService: PolicyService, router: Router) { + super(apiService, i18nService, cryptoService, messagingService, userService, passwordGenerationService, + platformUtilsService, folderService, cipherService, syncService, policyService, router); } async rotateEncKeyClicked() { @@ -198,13 +79,44 @@ export class ChangePasswordComponent implements OnInit { } } - 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]/)); + async setupSubmitActions() { + if (this.currentMasterPassword == null || this.currentMasterPassword === '') { + this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return false; + } + + if (this.rotateEncKey) { + await this.syncService.fullSync(true); + } + + super.setupSubmitActions(); + } + + async performSubmitActions(newMasterPasswordHash: string, newKey: SymmetricCryptoKey, + newEncKey: [SymmetricCryptoKey, CipherString]) { + const request = new PasswordRequest(); + request.masterPasswordHash = await this.cryptoService.hashPassword(this.currentMasterPassword, null); + request.newMasterPasswordHash = newMasterPasswordHash; + request.key = newEncKey[1].encryptedString; + + try { + if (this.rotateEncKey) { + this.formPromise = this.apiService.postPassword(request).then(() => { + return this.updateKey(newKey, request.newMasterPasswordHash); + }); + } else { + this.formPromise = this.apiService.postPassword(request); + } + + await this.formPromise; + + this.platformUtilsService.showToast('success', this.i18nService.t('masterPasswordChanged'), + this.i18nService.t('logBackIn')); + this.messagingService.send('logout'); + } catch { + this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred')); } - return userInput; } private async updateKey(key: SymmetricCryptoKey, masterPasswordHash: string) { diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 94a909adfd..cb11c4ae09 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -3164,6 +3164,12 @@ "taxInfoUpdated": { "message": "Tax information updated." }, + "setMasterPassword": { + "message": "Set Master Password" + }, + "ssoCompleteRegistration": { + "message": "In order to complete logging in with SSO, please set a master password below. Make sure to choose a strong password/passphrase and comply with all applied organizational policies." + }, "identifier": { "message": "Identifier" }