Merge pull request #6646 from ViacheslavKlimov/2fa-improvements
[3.4] 2FA improvements
This commit is contained in:
		
						commit
						a12a59d8c4
					
				@ -56,12 +56,31 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId) {
 | 
			
		||||
        PlatformTwoFaSettings platformTwoFaSettings = getPlatformTwoFaSettings(tenantId, true).orElse(null);
 | 
			
		||||
        return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
 | 
			
		||||
                .flatMap(userAuthSettings -> Optional.ofNullable(userAuthSettings.getTwoFaSettings()))
 | 
			
		||||
                .map(twoFaSettings -> {
 | 
			
		||||
                    twoFaSettings.getConfigs().keySet().removeIf(providerType -> {
 | 
			
		||||
                        return getTwoFaProviderConfig(tenantId, providerType).isEmpty();
 | 
			
		||||
                .map(userAuthSettings -> {
 | 
			
		||||
                    AccountTwoFaSettings twoFaSettings = userAuthSettings.getTwoFaSettings();
 | 
			
		||||
                    if (twoFaSettings == null) return null;
 | 
			
		||||
                    boolean updateNeeded;
 | 
			
		||||
 | 
			
		||||
                    Map<TwoFaProviderType, TwoFaAccountConfig> configs = twoFaSettings.getConfigs();
 | 
			
		||||
                    updateNeeded = configs.keySet().removeIf(providerType -> {
 | 
			
		||||
                        return platformTwoFaSettings == null || platformTwoFaSettings.getProviderConfig(providerType).isEmpty();
 | 
			
		||||
                    });
 | 
			
		||||
                    if (configs.size() == 1 && configs.containsKey(TwoFaProviderType.BACKUP_CODE)) {
 | 
			
		||||
                        configs.remove(TwoFaProviderType.BACKUP_CODE);
 | 
			
		||||
                        updateNeeded = true;
 | 
			
		||||
                    }
 | 
			
		||||
                    if (!configs.isEmpty() && configs.values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) {
 | 
			
		||||
                        configs.values().stream()
 | 
			
		||||
                                .filter(config -> config.getProviderType() != TwoFaProviderType.BACKUP_CODE)
 | 
			
		||||
                                .findFirst().ifPresent(config -> config.setUseByDefault(true));
 | 
			
		||||
                        updateNeeded = true;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (updateNeeded) {
 | 
			
		||||
                        twoFaSettings = saveAccountTwoFaSettings(tenantId, userId, twoFaSettings);
 | 
			
		||||
                    }
 | 
			
		||||
                    return twoFaSettings;
 | 
			
		||||
                });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -96,6 +96,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
 | 
			
		||||
 | 
			
		||||
        PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
 | 
			
		||||
        twoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig));
 | 
			
		||||
        twoFaSettings.setMinVerificationCodeSendPeriod(5);
 | 
			
		||||
        twoFaSettings.setVerificationCodeCheckRateLimit("3:900");
 | 
			
		||||
        twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
 | 
			
		||||
        twoFaSettings.setTotalAllowedTimeForVerification(3600);
 | 
			
		||||
@ -117,6 +118,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
 | 
			
		||||
        twoFaSettings.setVerificationCodeCheckRateLimit("0:12");
 | 
			
		||||
        twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(-1);
 | 
			
		||||
        twoFaSettings.setTotalAllowedTimeForVerification(0);
 | 
			
		||||
        twoFaSettings.setMinVerificationCodeSendPeriod(5);
 | 
			
		||||
 | 
			
		||||
        String errorMessage = getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
 | 
			
		||||
                .andExpect(status().isBadRequest()));
 | 
			
		||||
@ -156,6 +158,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
 | 
			
		||||
    private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception {
 | 
			
		||||
        PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
 | 
			
		||||
        twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig));
 | 
			
		||||
        twoFaSettings.setMinVerificationCodeSendPeriod(5);
 | 
			
		||||
        twoFaSettings.setTotalAllowedTimeForVerification(100);
 | 
			
		||||
 | 
			
		||||
        return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
 | 
			
		||||
                .andExpect(status().isBadRequest()));
 | 
			
		||||
@ -432,8 +436,9 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
 | 
			
		||||
 | 
			
		||||
    private void saveProvidersConfigs(TwoFaProviderConfig... providerConfigs) throws Exception {
 | 
			
		||||
        PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
 | 
			
		||||
 | 
			
		||||
        twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList()));
 | 
			
		||||
        twoFaSettings.setMinVerificationCodeSendPeriod(5);
 | 
			
		||||
        twoFaSettings.setTotalAllowedTimeForVerification(100);
 | 
			
		||||
        doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -355,6 +355,8 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
 | 
			
		||||
        emailTwoFaProviderConfig.setVerificationCodeLifetime(60);
 | 
			
		||||
 | 
			
		||||
        platformTwoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig, emailTwoFaProviderConfig));
 | 
			
		||||
        platformTwoFaSettings.setMinVerificationCodeSendPeriod(5);
 | 
			
		||||
        platformTwoFaSettings.setTotalAllowedTimeForVerification(100);
 | 
			
		||||
        twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, platformTwoFaSettings);
 | 
			
		||||
 | 
			
		||||
        User twoFaUser = new User();
 | 
			
		||||
@ -409,6 +411,8 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
 | 
			
		||||
 | 
			
		||||
        PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
 | 
			
		||||
        twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList()));
 | 
			
		||||
        twoFaSettings.setMinVerificationCodeSendPeriod(5);
 | 
			
		||||
        twoFaSettings.setTotalAllowedTimeForVerification(100);
 | 
			
		||||
        Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings));
 | 
			
		||||
        twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
 | 
			
		||||
 | 
			
		||||
@ -425,6 +429,8 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
 | 
			
		||||
 | 
			
		||||
        PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
 | 
			
		||||
        twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList()));
 | 
			
		||||
        twoFaSettings.setMinVerificationCodeSendPeriod(5);
 | 
			
		||||
        twoFaSettings.setTotalAllowedTimeForVerification(100);
 | 
			
		||||
        twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
 | 
			
		||||
 | 
			
		||||
        SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
 | 
			
		||||
 | 
			
		||||
@ -35,12 +35,14 @@ public class PlatformTwoFaSettings {
 | 
			
		||||
    @NotNull
 | 
			
		||||
    private List<TwoFaProviderConfig> providers;
 | 
			
		||||
 | 
			
		||||
    @NotNull
 | 
			
		||||
    @Min(value = 5, message = "minimum verification code sent period must be greater than or equal 5")
 | 
			
		||||
    private Integer minVerificationCodeSendPeriod;
 | 
			
		||||
    @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid")
 | 
			
		||||
    private String verificationCodeCheckRateLimit;
 | 
			
		||||
    @Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
 | 
			
		||||
    private Integer maxVerificationFailuresBeforeUserLockout;
 | 
			
		||||
    @NotNull
 | 
			
		||||
    @Min(value = 60, message = "total amount of time allotted for verification must be greater than or equal 60")
 | 
			
		||||
    private Integer totalAllowedTimeForVerification;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -376,7 +376,7 @@ export class MenuService {
 | 
			
		||||
        name: 'admin.system-settings',
 | 
			
		||||
        type: 'toggle',
 | 
			
		||||
        path: '/settings',
 | 
			
		||||
        height: '120px',
 | 
			
		||||
        height: '80px',
 | 
			
		||||
        icon: 'settings',
 | 
			
		||||
        pages: [
 | 
			
		||||
          {
 | 
			
		||||
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2022 The Thingsboard Authors
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
@ -29,6 +29,7 @@ import { DatePipe } from '@angular/common';
 | 
			
		||||
import { ClipboardService } from 'ngx-clipboard';
 | 
			
		||||
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
 | 
			
		||||
import {
 | 
			
		||||
  AccountTwoFaSettingProviders,
 | 
			
		||||
  AccountTwoFaSettings,
 | 
			
		||||
  BackupCodeTwoFactorAuthAccountConfig,
 | 
			
		||||
  EmailTwoFactorAuthAccountConfig,
 | 
			
		||||
@ -49,7 +50,7 @@ import { isDefinedAndNotNull } from '@core/utils';
 | 
			
		||||
export class SecurityComponent extends PageComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  private readonly destroy$ = new Subject<void>();
 | 
			
		||||
  private accountConfig: AccountTwoFaSettings;
 | 
			
		||||
  private accountConfig: AccountTwoFaSettingProviders;
 | 
			
		||||
 | 
			
		||||
  twoFactorAuth: FormGroup;
 | 
			
		||||
  user: User;
 | 
			
		||||
@ -128,12 +129,11 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private processTwoFactorAuthConfig(setting: AccountTwoFaSettings) {
 | 
			
		||||
    this.accountConfig = setting;
 | 
			
		||||
    const configs = this.accountConfig.configs;
 | 
			
		||||
    this.accountConfig = setting?.configs || {};
 | 
			
		||||
    Object.values(TwoFactorAuthProviderType).forEach(provider => {
 | 
			
		||||
      if (configs[provider]) {
 | 
			
		||||
      if (this.accountConfig[provider]) {
 | 
			
		||||
        this.twoFactorAuth.get(provider).setValue(true);
 | 
			
		||||
        if (configs[provider].useByDefault) {
 | 
			
		||||
        if (this.accountConfig[provider].useByDefault) {
 | 
			
		||||
          this.useByDefault = provider;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
@ -216,7 +216,7 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  generateNewBackupCode() {
 | 
			
		||||
    const codeLeft = this.accountConfig.configs[TwoFactorAuthProviderType.BACKUP_CODE].codesLeft;
 | 
			
		||||
    const codeLeft = (this.accountConfig[TwoFactorAuthProviderType.BACKUP_CODE] as BackupCodeTwoFactorAuthAccountConfig).codesLeft;
 | 
			
		||||
    let subscription: Observable<boolean>;
 | 
			
		||||
    if (codeLeft) {
 | 
			
		||||
      subscription = this.dialogService.confirm(
 | 
			
		||||
@ -240,7 +240,7 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
 | 
			
		||||
 | 
			
		||||
  providerDataInfo(provider: TwoFactorAuthProviderType) {
 | 
			
		||||
    const info = {info: null};
 | 
			
		||||
    const providerConfig = this.accountConfig.configs[provider];
 | 
			
		||||
    const providerConfig = this.accountConfig[provider];
 | 
			
		||||
    if (isDefinedAndNotNull(providerConfig)) {
 | 
			
		||||
      switch (provider) {
 | 
			
		||||
        case TwoFactorAuthProviderType.EMAIL:
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ import {
 | 
			
		||||
import { TranslateService } from '@ngx-translate/core';
 | 
			
		||||
import { interval, Subscription } from 'rxjs';
 | 
			
		||||
import { isEqual } from '@core/utils';
 | 
			
		||||
import { ActionNotificationShow } from '@core/notification/notification.actions';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'tb-two-factor-auth-login',
 | 
			
		||||
@ -118,6 +119,13 @@ export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit
 | 
			
		||||
              }
 | 
			
		||||
              this.verificationForm.get('verificationCode').setErrors(errors);
 | 
			
		||||
            }, 5000);
 | 
			
		||||
          } else {
 | 
			
		||||
            this.store.dispatch(new ActionNotificationShow({
 | 
			
		||||
              message: error.error.message,
 | 
			
		||||
              type: 'error',
 | 
			
		||||
              verticalPosition: 'top',
 | 
			
		||||
              horizontalPosition: 'left'
 | 
			
		||||
            }));
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
@ -88,11 +88,14 @@ export interface BackupCodeTwoFactorAuthAccountConfig extends GeneralTwoFactorAu
 | 
			
		||||
export type TwoFactorAuthAccountConfig = TotpTwoFactorAuthAccountConfig | SmsTwoFactorAuthAccountConfig |
 | 
			
		||||
  EmailTwoFactorAuthAccountConfig | BackupCodeTwoFactorAuthAccountConfig;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export interface AccountTwoFaSettings {
 | 
			
		||||
  configs: {TwoFactorAuthProviderType: TwoFactorAuthAccountConfig};
 | 
			
		||||
  configs: AccountTwoFaSettingProviders;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AccountTwoFaSettingProviders = {
 | 
			
		||||
  [key in TwoFactorAuthProviderType]?: TwoFactorAuthAccountConfig;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface TwoFaProviderInfo {
 | 
			
		||||
  type: TwoFactorAuthProviderType;
 | 
			
		||||
  default: boolean;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user