diff --git a/src/app/accounts/change-password.component.html b/src/app/accounts/change-password.component.html
new file mode 100644
index 00000000000..460120a49df
--- /dev/null
+++ b/src/app/accounts/change-password.component.html
@@ -0,0 +1,45 @@
+
+{{'ssoCompleteRegistration' | i18n}}
+
+ {{'masterPasswordPolicyInEffect' | i18n}}
+
+ - 0">
+ {{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
+
+ - 0">
+ {{'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 00000000000..84e55c6998c
--- /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 cf473258b94..f906d4f95e3 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 87d4e43f417..7cae81f3889 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 b79bb73c59d..9053805b52d 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 b7627823911..7ea8c2f3d92 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 079877a6521..5997481282a 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 94a909adfd1..cb11c4ae099 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"
}