Browse Source

Merge pull request #14367 from thingsboard/fix/2fa-enforce

Fixes for 2FA enforcement
pull/14404/head
Viacheslav Klimov 7 months ago
committed by GitHub
parent
commit
dfa6736edb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 22
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java
  2. 5
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html
  3. 7
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts
  4. 15
      ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html
  5. 39
      ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts
  6. 5
      ui-ngx/src/assets/locale/locale.constant-en_US.json

22
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java

@ -16,7 +16,6 @@
package org.thingsboard.server.service.security.auth.mfa.config;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
@ -49,12 +48,11 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
private final UserAuthSettingsDao userAuthSettingsDao;
private final AdminSettingsService adminSettingsService;
private final AdminSettingsDao adminSettingsDao;
@Autowired @Lazy
private TwoFactorAuthService twoFactorAuthService;
@Lazy
private final TwoFactorAuthService twoFactorAuthService;
protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings";
@Override
public Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, User user) {
PlatformTwoFaSettings platformTwoFaSettings = getPlatformTwoFaSettings(tenantId, true).orElse(null);
@ -87,11 +85,6 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
}
protected AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, User user, AccountTwoFaSettings settings) {
if (settings.getConfigs().isEmpty()) {
if (twoFactorAuthService.isEnforceTwoFaEnabled(tenantId, user)) {
throw new DataValidationException("At least one 2FA provider is required");
}
}
UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(user.getId()))
.orElseGet(() -> {
UserAuthSettings newUserAuthSettings = new UserAuthSettings();
@ -105,7 +98,6 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
return settings;
}
@Override
public Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType) {
return getAccountTwoFaSettings(tenantId, user)
@ -134,6 +126,7 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
if (configs.values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) {
configs.values().stream().findFirst().ifPresent(config -> config.setUseByDefault(true));
}
checkAccountTwoFaSettings(tenantId, user, settings);
return saveAccountTwoFaSettings(tenantId, user, settings);
}
@ -151,6 +144,7 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
.min(Comparator.comparing(TwoFaAccountConfig::getProviderType))
.ifPresent(config -> config.setUseByDefault(true));
}
checkAccountTwoFaSettings(tenantId, user, settings);
return saveAccountTwoFaSettings(tenantId, user, settings);
}
@ -203,4 +197,12 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
.ifPresent(adminSettings -> adminSettingsDao.removeById(tenantId, adminSettings.getId().getId()));
}
private void checkAccountTwoFaSettings(TenantId tenantId, User user, AccountTwoFaSettings settings) {
if (settings.getConfigs().isEmpty()) {
if (twoFactorAuthService.isEnforceTwoFaEnabled(tenantId, user)) {
throw new DataValidationException("At least one 2FA provider is required");
}
}
}
}

5
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html

@ -90,6 +90,9 @@
<section class="tb-form-panel stroked" formArrayName="providers">
<div class="tb-form-panel-title" translate>admin.2fa.available-providers</div>
@if (!atListOneProvider) {
<tb-error noMargin error="{{ 'admin.2fa.available-providers-required' | translate }}" class="flex h-4 items-center pl-3"/>
}
<ng-container *ngFor="let provider of providersForm.controls; let i = index; trackBy: trackByElement">
<div class="tb-form-panel stroked tb-slide-toggle">
<mat-expansion-panel class="tb-settings" [formGroupName]="i">
@ -243,7 +246,7 @@
</div>
<div class="flex flex-row items-center justify-end gap-2">
<button mat-button mat-raised-button color="primary"
[disabled]="(isLoading$ | async) || twoFaFormGroup.invalid || !twoFaFormGroup.dirty"
[disabled]="(isLoading$ | async) || twoFaFormGroup.invalid || !twoFaFormGroup.dirty || !atListOneProvider"
type="submit">
{{'action.save' | translate}}
</button>

7
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts

@ -184,6 +184,13 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
});
}
get atListOneProvider():boolean {
if (this.twoFaFormGroup.get('enforceTwoFa').value) {
return this.providersForm.value.some(value => value.enable);
}
return true;
}
private setAuthConfigFormValue(settings: TwoFactorAuthSettings) {
const [checkRateLimitNumber, checkRateLimitTime] = this.splitRateLimit(settings?.verificationCodeCheckRateLimit);
const allowProvidersConfig = settings?.providers.map(provider => provider.providerType) || [];

15
ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html

@ -262,9 +262,18 @@
inputmode="numeric" pattern="[0-9]*"
autocomplete="off"
placeholder="{{ 'login.verification-code' | translate }}">
<mat-error *ngIf="configForm.get('verificationCode').invalid">
{{ 'login.verification-code-invalid' | translate }}
</mat-error>
@if (configForm.get('verificationCode').getError('required') ||
configForm.get('verificationCode').getError('minlength') ||
configForm.get('verificationCode').getError('maxlength') ||
configForm.get('verificationCode').getError('pattern')) {
<mat-error>{{ 'login.verification-code-invalid' | translate }}</mat-error>
}
@if (configForm.get('verificationCode').getError('incorrectCode')) {
<mat-error>{{ 'login.verification-code-incorrect' | translate }}</mat-error>
}
@if (configForm.get('verificationCode').getError('tooManyRequest')) {
<mat-error>{{ 'login.verification-code-many-request' | translate }}</mat-error>
}
</mat-form-field>
<div class="flex flex-col items-center justify-start gap-2 w-full">
<button type="button" mat-flat-button color="accent" [disabled]="(isLoading$ | async) || configForm.invalid || !configForm.dirty" class="navigation w-full" (click)="saveConfig(providerType)">

39
ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.ts

@ -239,22 +239,31 @@ export class ForceTwoFactorAuthLoginComponent extends PageComponent implements O
saveConfig(type: TwoFactorAuthProviderType) {
if (this.configForm.valid) {
this.twoFaService.verifyAndSaveTwoFaAccountConfig(this.authAccountConfig,
this.configForm.get('verificationCode').value).subscribe((config) => {
switch (type) {
case TwoFactorAuthProviderType.TOTP:
this.appState.set(ProvidersState.SUCCESS);
break;
case TwoFactorAuthProviderType.SMS:
this.smsState.set(ProvidersState.SUCCESS);
break;
case TwoFactorAuthProviderType.EMAIL:
this.emailState.set(ProvidersState.SUCCESS);
break;
this.configForm.get('verificationCode').value).subscribe({
next: (config) => {
switch (type) {
case TwoFactorAuthProviderType.TOTP:
this.appState.set(ProvidersState.SUCCESS);
break;
case TwoFactorAuthProviderType.SMS:
this.smsState.set(ProvidersState.SUCCESS);
break;
case TwoFactorAuthProviderType.EMAIL:
this.emailState.set(ProvidersState.SUCCESS);
break;
}
this.config = config;
this.authAccountConfig = null;
this.allowedProviders();
},
error: error => {
if (error.status === 400) {
this.configForm.get('verificationCode').setErrors({incorrectCode: true});
} else if (error.status === 429) {
this.configForm.get('verificationCode').setErrors({tooManyRequest: true});
}
}
this.config = config;
this.authAccountConfig = null;
this.allowedProviders();
});
})
}
}

5
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -485,6 +485,7 @@
"2fa": {
"2fa": "Two-factor authentication",
"available-providers": "Available providers",
"available-providers-required": "At least one 2FA provider should be configured.",
"issuer-name": "Issuer name",
"issuer-name-required": "Issuer name is required.",
"max-verification-failures-before-user-lockout": "Max verification failures before user lockout",
@ -4268,6 +4269,8 @@
"print": "Print",
"verification-code": "6-digit code",
"verification-code-invalid": "Invalid verification code format",
"verification-code-incorrect": "Verification code is incorrect",
"verification-code-many-request": "Too many requests to check verification code",
"scan-qr-code": "Scan this QR code with your verification app",
"phone-input": {
"phone-input-label": "Phone number",
@ -4843,7 +4846,7 @@
"verification-code": "6-digit code",
"verification-code-invalid": "Invalid verification code format",
"verification-code-incorrect": "Verification code is incorrect",
"verification-code-many-request": "Too many requests check verification code",
"verification-code-many-request": "Too many requests to check verification code",
"verification-step-description": "Enter a 6-digit code we just sent to {{address}}",
"verification-step-label": "Verification"
},

Loading…
Cancel
Save