enforce 2FA feature draft
This commit is contained in:
		
							parent
							
								
									c34574ae1a
								
							
						
					
					
						commit
						b1d19eb3cc
					
				@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
 | 
			
		||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
 | 
			
		||||
import org.thingsboard.server.common.data.id.TenantId;
 | 
			
		||||
import org.thingsboard.server.common.data.limit.LimitedApi;
 | 
			
		||||
import org.thingsboard.server.common.data.security.Authority;
 | 
			
		||||
import org.thingsboard.server.common.data.security.UserCredentials;
 | 
			
		||||
import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent;
 | 
			
		||||
import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent;
 | 
			
		||||
@ -48,7 +49,9 @@ import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
 | 
			
		||||
import org.thingsboard.server.config.annotations.ApiOperation;
 | 
			
		||||
import org.thingsboard.server.dao.settings.SecuritySettingsService;
 | 
			
		||||
import org.thingsboard.server.queue.util.TbCoreComponent;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.rest.RestAwareAuthenticationSuccessHandler;
 | 
			
		||||
import org.thingsboard.server.service.security.model.ActivateUserRequest;
 | 
			
		||||
import org.thingsboard.server.service.security.model.ChangePasswordRequest;
 | 
			
		||||
import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest;
 | 
			
		||||
@ -74,7 +77,8 @@ public class AuthController extends BaseController {
 | 
			
		||||
    private final SecuritySettingsService securitySettingsService;
 | 
			
		||||
    private final RateLimitService rateLimitService;
 | 
			
		||||
    private final ApplicationEventPublisher eventPublisher;
 | 
			
		||||
 | 
			
		||||
    private final TwoFactorAuthService twoFactorAuthService;
 | 
			
		||||
    private final RestAwareAuthenticationSuccessHandler authenticationSuccessHandler;
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Get current User (getUser)",
 | 
			
		||||
            notes = "Get the information about the User which credentials are used to perform this REST API call.")
 | 
			
		||||
@ -221,7 +225,13 @@ public class AuthController extends BaseController {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var tokenPair = tokenFactory.createTokenPair(securityUser);
 | 
			
		||||
        JwtPair tokenPair;
 | 
			
		||||
        if (twoFactorAuthService.isEnforceTwoFaEnabled(securityUser.getTenantId())) {
 | 
			
		||||
            tokenPair = authenticationSuccessHandler.createMfaTokenPair(securityUser, Authority.ENFORCE_MFA_TOKEN);
 | 
			
		||||
        } else {
 | 
			
		||||
            tokenPair = tokenFactory.createTokenPair(securityUser);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(request), ActionType.LOGIN, null);
 | 
			
		||||
        return tokenPair;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -56,7 +56,6 @@ public class TwoFactorAuthConfigController extends BaseController {
 | 
			
		||||
    private final TwoFaConfigManager twoFaConfigManager;
 | 
			
		||||
    private final TwoFactorAuthService twoFactorAuthService;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Get account 2FA settings (getAccountTwoFaSettings)",
 | 
			
		||||
            notes = "Get user's account 2FA configuration. Configuration contains configs for different 2FA providers." + NEW_LINE +
 | 
			
		||||
                    "Example:\n" +
 | 
			
		||||
@ -73,7 +72,6 @@ public class TwoFactorAuthConfigController extends BaseController {
 | 
			
		||||
        return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()).orElse(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)",
 | 
			
		||||
            notes = "Generate new 2FA account config template for specified provider type. " + NEW_LINE +
 | 
			
		||||
                    "For TOTP, this will return a corresponding account config template " +
 | 
			
		||||
@ -99,7 +97,7 @@ public class TwoFactorAuthConfigController extends BaseController {
 | 
			
		||||
                    "Will throw an error (Bad Request) if the provider is not configured for usage. " +
 | 
			
		||||
                    ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
 | 
			
		||||
    @PostMapping("/account/config/generate")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'ENFORCE_MFA_TOKEN')")
 | 
			
		||||
    public TwoFaAccountConfig generateTwoFaAccountConfig(@Parameter(description = "2FA provider type to generate new account config for", schema = @Schema(defaultValue = "TOTP", requiredMode = Schema.RequiredMode.REQUIRED))
 | 
			
		||||
                                                         @RequestParam TwoFaProviderType providerType) throws Exception {
 | 
			
		||||
        SecurityUser user = getCurrentUser();
 | 
			
		||||
@ -139,7 +137,7 @@ public class TwoFactorAuthConfigController extends BaseController {
 | 
			
		||||
                    "Will throw an error (Bad Request) if the provider is not configured for usage. " +
 | 
			
		||||
                    ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
 | 
			
		||||
    @PostMapping("/account/config")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'ENFORCE_MFA_TOKEN')")
 | 
			
		||||
    public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig,
 | 
			
		||||
                                                                @RequestParam(required = false) String verificationCode) throws Exception {
 | 
			
		||||
        SecurityUser user = getCurrentUser();
 | 
			
		||||
@ -189,7 +187,6 @@ public class TwoFactorAuthConfigController extends BaseController {
 | 
			
		||||
        return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
 | 
			
		||||
            "Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" +
 | 
			
		||||
                    "Example of response:\n" +
 | 
			
		||||
@ -197,7 +194,7 @@ public class TwoFactorAuthConfigController extends BaseController {
 | 
			
		||||
                    ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER
 | 
			
		||||
    )
 | 
			
		||||
    @GetMapping("/providers")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'ENFORCE_MFA_TOKEN')")
 | 
			
		||||
    public List<TwoFaProviderType> getAvailableTwoFaProviders() throws ThingsboardException {
 | 
			
		||||
        return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true)
 | 
			
		||||
                .map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream()
 | 
			
		||||
@ -205,7 +202,6 @@ public class TwoFactorAuthConfigController extends BaseController {
 | 
			
		||||
                .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Get platform 2FA settings (getPlatformTwoFaSettings)",
 | 
			
		||||
            notes = "Get platform settings for 2FA. The settings are described for savePlatformTwoFaSettings API method. " +
 | 
			
		||||
                    "If 2FA is not configured, then an empty response will be returned." +
 | 
			
		||||
@ -260,11 +256,10 @@ public class TwoFactorAuthConfigController extends BaseController {
 | 
			
		||||
    @PostMapping("/settings")
 | 
			
		||||
    @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
 | 
			
		||||
    public PlatformTwoFaSettings savePlatformTwoFaSettings(@Parameter(description = "Settings value", required = true)
 | 
			
		||||
                                          @RequestBody PlatformTwoFaSettings twoFaSettings) throws ThingsboardException {
 | 
			
		||||
                                                           @RequestBody PlatformTwoFaSettings twoFaSettings) throws ThingsboardException {
 | 
			
		||||
        return twoFaConfigManager.savePlatformTwoFaSettings(getTenantId(), twoFaSettings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Data
 | 
			
		||||
    public static class TwoFaAccountConfigUpdateRequest {
 | 
			
		||||
        private boolean useByDefault;
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,6 @@ public class TwoFactorAuthController extends BaseController {
 | 
			
		||||
    private final SystemSecurityService systemSecurityService;
 | 
			
		||||
    private final UserService userService;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Request 2FA verification code (requestTwoFaVerificationCode)",
 | 
			
		||||
            notes = "Request 2FA verification code." + NEW_LINE +
 | 
			
		||||
                    "To make a request to this endpoint, you need an access token with the scope of PRE_VERIFICATION_TOKEN, " +
 | 
			
		||||
@ -91,18 +90,9 @@ public class TwoFactorAuthController extends BaseController {
 | 
			
		||||
                                              @RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception {
 | 
			
		||||
        SecurityUser user = getCurrentUser();
 | 
			
		||||
        boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true);
 | 
			
		||||
        if (verificationSuccess) {
 | 
			
		||||
            systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null);
 | 
			
		||||
            user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal());
 | 
			
		||||
            return tokenFactory.createTokenPair(user);
 | 
			
		||||
        } else {
 | 
			
		||||
            ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
 | 
			
		||||
            systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error);
 | 
			
		||||
            throw error;
 | 
			
		||||
        }
 | 
			
		||||
        return getRegularJwtPair(servletRequest, user, verificationSuccess, "Verification code is incorrect");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
 | 
			
		||||
            "Get the list of 2FA provider infos available for user to use. Example:\n" +
 | 
			
		||||
                    "```\n[\n" +
 | 
			
		||||
@ -139,6 +129,28 @@ public class TwoFactorAuthController extends BaseController {
 | 
			
		||||
                .collect(Collectors.toList());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @ApiOperation(value = "Get regular token pair after successfully saved two factor settings",
 | 
			
		||||
            notes = "Checks 2FA setting saved, and if it success the method returns a regular access and refresh token pair.")
 | 
			
		||||
    @PostMapping("/login")
 | 
			
		||||
    @PreAuthorize("hasAuthority('ENFORCE_MFA_TOKEN')")
 | 
			
		||||
    public JwtPair authorizeByTwoFaEnforceToken(HttpServletRequest servletRequest) throws ThingsboardException {
 | 
			
		||||
        SecurityUser user = getCurrentUser();
 | 
			
		||||
        boolean isEnabled = twoFactorAuthService.isTwoFaEnabled(user.getTenantId(), user.getId());
 | 
			
		||||
        return getRegularJwtPair(servletRequest, user, isEnabled, "Two factor settings is not set up!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private JwtPair getRegularJwtPair(HttpServletRequest servletRequest, SecurityUser user, boolean isAvailable, String errorMessage) throws ThingsboardException {
 | 
			
		||||
        if (isAvailable) {
 | 
			
		||||
            systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null);
 | 
			
		||||
            user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal());
 | 
			
		||||
            return tokenFactory.createTokenPair(user);
 | 
			
		||||
        } else {
 | 
			
		||||
            ThingsboardException error = new ThingsboardException(errorMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
 | 
			
		||||
            systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error);
 | 
			
		||||
            throw error;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Data
 | 
			
		||||
    @AllArgsConstructor
 | 
			
		||||
    @Builder
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,24 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright © 2016-2024 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.
 | 
			
		||||
 */
 | 
			
		||||
package org.thingsboard.server.service.security.auth;
 | 
			
		||||
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
 | 
			
		||||
public class ForceMfaAuthenticationToken extends AbstractJwtAuthenticationToken {
 | 
			
		||||
    public ForceMfaAuthenticationToken(SecurityUser securityUser) {
 | 
			
		||||
        super(securityUser);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -67,6 +67,13 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
 | 
			
		||||
                .orElse(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public boolean isEnforceTwoFaEnabled(TenantId tenantId) {
 | 
			
		||||
        return configManager.getPlatformTwoFaSettings(tenantId, true)
 | 
			
		||||
                .map(PlatformTwoFaSettings::isEnforceTwoFa)
 | 
			
		||||
                .orElse(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException {
 | 
			
		||||
        getTwoFaProvider(providerType).check(tenantId);
 | 
			
		||||
 | 
			
		||||
@ -27,8 +27,9 @@ public interface TwoFactorAuthService {
 | 
			
		||||
 | 
			
		||||
    boolean isTwoFaEnabled(TenantId tenantId, UserId userId);
 | 
			
		||||
 | 
			
		||||
    void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException;
 | 
			
		||||
    boolean isEnforceTwoFaEnabled(TenantId tenantId);
 | 
			
		||||
 | 
			
		||||
    void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException;
 | 
			
		||||
 | 
			
		||||
    void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoF
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
 | 
			
		||||
import org.thingsboard.server.dao.exception.DataValidationException;
 | 
			
		||||
import org.thingsboard.server.dao.service.ConstraintValidator;
 | 
			
		||||
import org.thingsboard.server.dao.settings.AdminSettingsDao;
 | 
			
		||||
import org.thingsboard.server.dao.settings.AdminSettingsService;
 | 
			
		||||
@ -166,7 +167,9 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
 | 
			
		||||
        for (TwoFaProviderConfig providerConfig : twoFactorAuthSettings.getProviders()) {
 | 
			
		||||
            twoFactorAuthService.checkProvider(tenantId, providerConfig.getProviderType());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (twoFactorAuthSettings.isEnforceTwoFa() && twoFactorAuthSettings.getProviders().isEmpty()) {
 | 
			
		||||
            throw new DataValidationException("At least one 2FA provider is required if enforce enabled!");
 | 
			
		||||
        }
 | 
			
		||||
        AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
 | 
			
		||||
                .orElseGet(() -> {
 | 
			
		||||
                    AdminSettings newSettings = new AdminSettings();
 | 
			
		||||
 | 
			
		||||
@ -43,6 +43,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
 | 
			
		||||
import org.thingsboard.server.dao.settings.SecuritySettingsService;
 | 
			
		||||
import org.thingsboard.server.dao.user.UserService;
 | 
			
		||||
import org.thingsboard.server.queue.util.TbCoreComponent;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.ForceMfaAuthenticationToken;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
 | 
			
		||||
import org.thingsboard.server.service.security.exception.UserPasswordNotValidException;
 | 
			
		||||
@ -105,6 +106,8 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
 | 
			
		||||
            securityUser = authenticateByUsernameAndPassword(authentication, userPrincipal, username, password);
 | 
			
		||||
            if (twoFactorAuthService.isTwoFaEnabled(securityUser.getTenantId(), securityUser.getId())) {
 | 
			
		||||
                return new MfaAuthenticationToken(securityUser);
 | 
			
		||||
            } else if (twoFactorAuthService.isEnforceTwoFaEnabled(securityUser.getTenantId())) {
 | 
			
		||||
                return new ForceMfaAuthenticationToken(securityUser);
 | 
			
		||||
            } else {
 | 
			
		||||
                systemSecurityService.logLoginAction(securityUser, authentication.getDetails(), ActionType.LOGIN, null);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ import org.springframework.stereotype.Component;
 | 
			
		||||
import org.thingsboard.common.util.JacksonUtil;
 | 
			
		||||
import org.thingsboard.server.common.data.security.Authority;
 | 
			
		||||
import org.thingsboard.server.common.data.security.model.JwtPair;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.ForceMfaAuthenticationToken;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
 | 
			
		||||
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
 | 
			
		||||
import org.thingsboard.server.service.security.model.SecurityUser;
 | 
			
		||||
@ -48,16 +49,13 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
 | 
			
		||||
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
 | 
			
		||||
                                        Authentication authentication) throws IOException, ServletException {
 | 
			
		||||
        SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
 | 
			
		||||
        JwtPair tokenPair = new JwtPair();
 | 
			
		||||
        JwtPair tokenPair;
 | 
			
		||||
 | 
			
		||||
        if (authentication instanceof MfaAuthenticationToken) {
 | 
			
		||||
            int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true)
 | 
			
		||||
                    .flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())
 | 
			
		||||
                            .filter(time -> time > 0))
 | 
			
		||||
                    .orElse((int) TimeUnit.MINUTES.toSeconds(30));
 | 
			
		||||
            tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken());
 | 
			
		||||
            tokenPair.setRefreshToken(null);
 | 
			
		||||
            tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN);
 | 
			
		||||
            tokenPair = createMfaTokenPair(securityUser, Authority.PRE_VERIFICATION_TOKEN);
 | 
			
		||||
        }
 | 
			
		||||
        else if (authentication instanceof ForceMfaAuthenticationToken) {
 | 
			
		||||
            tokenPair = createMfaTokenPair(securityUser, Authority.ENFORCE_MFA_TOKEN);
 | 
			
		||||
        } else {
 | 
			
		||||
            tokenPair = tokenFactory.createTokenPair(securityUser);
 | 
			
		||||
        }
 | 
			
		||||
@ -69,6 +67,18 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
 | 
			
		||||
        clearAuthenticationAttributes(request);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public JwtPair createMfaTokenPair(SecurityUser securityUser, Authority scope) {
 | 
			
		||||
        JwtPair tokenPair = new JwtPair();
 | 
			
		||||
        int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true)
 | 
			
		||||
                .flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())
 | 
			
		||||
                        .filter(time -> time > 0))
 | 
			
		||||
                .orElse((int) TimeUnit.MINUTES.toSeconds(30));
 | 
			
		||||
        tokenPair.setToken(tokenFactory.createMfaToken(securityUser, scope, preVerificationTokenLifetime).getToken());
 | 
			
		||||
        tokenPair.setRefreshToken(null);
 | 
			
		||||
        tokenPair.setScope(scope);
 | 
			
		||||
        return tokenPair;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Removes temporary authentication-related data which may have been stored
 | 
			
		||||
     * in the session during the authentication process..
 | 
			
		||||
 | 
			
		||||
@ -115,13 +115,16 @@ public class JwtTokenFactory {
 | 
			
		||||
            throw new IllegalArgumentException("JWT Token doesn't have any scopes");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Authority authority = Authority.parse(scopes.get(0));
 | 
			
		||||
 | 
			
		||||
        SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
 | 
			
		||||
        securityUser.setEmail(subject);
 | 
			
		||||
        securityUser.setAuthority(Authority.parse(scopes.get(0)));
 | 
			
		||||
        securityUser.setAuthority(authority);
 | 
			
		||||
        String tenantId = claims.get(TENANT_ID, String.class);
 | 
			
		||||
 | 
			
		||||
        if (tenantId != null) {
 | 
			
		||||
            securityUser.setTenantId(TenantId.fromUUID(UUID.fromString(tenantId)));
 | 
			
		||||
        } else if (securityUser.getAuthority() == Authority.SYS_ADMIN) {
 | 
			
		||||
        } else if (authority == Authority.SYS_ADMIN) {
 | 
			
		||||
            securityUser.setTenantId(TenantId.SYS_TENANT_ID);
 | 
			
		||||
        }
 | 
			
		||||
        String customerId = claims.get(CUSTOMER_ID, String.class);
 | 
			
		||||
@ -133,7 +136,7 @@ public class JwtTokenFactory {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        UserPrincipal principal;
 | 
			
		||||
        if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) {
 | 
			
		||||
        if (authority != Authority.PRE_VERIFICATION_TOKEN && authority != Authority.ENFORCE_MFA_TOKEN) {
 | 
			
		||||
            securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
 | 
			
		||||
            securityUser.setLastName(claims.get(LAST_NAME, String.class));
 | 
			
		||||
            securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
 | 
			
		||||
@ -179,8 +182,8 @@ public class JwtTokenFactory {
 | 
			
		||||
        return securityUser;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public JwtToken createPreVerificationToken(SecurityUser user, Integer expirationTime) {
 | 
			
		||||
        JwtBuilder jwtBuilder = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), expirationTime)
 | 
			
		||||
    public JwtToken createMfaToken(SecurityUser user, Authority scope, Integer expirationTime) {
 | 
			
		||||
        JwtBuilder jwtBuilder = setUpToken(user, Collections.singletonList(scope.name()), expirationTime)
 | 
			
		||||
                .claim(TENANT_ID, user.getTenantId().toString());
 | 
			
		||||
        if (user.getCustomerId() != null) {
 | 
			
		||||
            jwtBuilder.claim(CUSTOMER_ID, user.getCustomerId().toString());
 | 
			
		||||
 | 
			
		||||
@ -85,7 +85,6 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest {
 | 
			
		||||
        twoFaConfigManager.deletePlatformTwoFaSettings(tenantId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSavePlatformTwoFaSettings() throws Exception {
 | 
			
		||||
        loginSysAdmin();
 | 
			
		||||
@ -102,6 +101,7 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest {
 | 
			
		||||
        twoFaSettings.setVerificationCodeCheckRateLimit("3:900");
 | 
			
		||||
        twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
 | 
			
		||||
        twoFaSettings.setTotalAllowedTimeForVerification(3600);
 | 
			
		||||
        twoFaSettings.setEnforceTwoFa(true);
 | 
			
		||||
 | 
			
		||||
        doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
 | 
			
		||||
 | 
			
		||||
@ -111,6 +111,21 @@ public class TwoFactorAuthConfigTest extends AbstractControllerTest {
 | 
			
		||||
        assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSavePlatformTwoFaSettingsWithEnforceTwoFaWithoutProviders() throws Exception {
 | 
			
		||||
        loginSysAdmin();
 | 
			
		||||
 | 
			
		||||
        PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
 | 
			
		||||
        twoFaSettings.setProviders(List.of());
 | 
			
		||||
        twoFaSettings.setMinVerificationCodeSendPeriod(5);
 | 
			
		||||
        twoFaSettings.setVerificationCodeCheckRateLimit("3:900");
 | 
			
		||||
        twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
 | 
			
		||||
        twoFaSettings.setTotalAllowedTimeForVerification(3600);
 | 
			
		||||
        twoFaSettings.setEnforceTwoFa(true);
 | 
			
		||||
 | 
			
		||||
        doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isBadRequest());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testSavePlatformTwoFaSettings_validationError() throws Exception {
 | 
			
		||||
        loginSysAdmin();
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ import org.mockito.ArgumentCaptor;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.springframework.boot.test.mock.mockito.MockBean;
 | 
			
		||||
import org.springframework.boot.test.mock.mockito.SpyBean;
 | 
			
		||||
import org.springframework.web.util.UriComponentsBuilder;
 | 
			
		||||
import org.thingsboard.rule.engine.api.SmsService;
 | 
			
		||||
import org.thingsboard.server.common.data.StringUtils;
 | 
			
		||||
import org.thingsboard.server.common.data.User;
 | 
			
		||||
@ -66,6 +67,10 @@ import java.util.stream.Stream;
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.awaitility.Awaitility.await;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertNull;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.assertTrue;
 | 
			
		||||
import static org.junit.jupiter.api.Assertions.fail;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.any;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.eq;
 | 
			
		||||
@ -395,6 +400,42 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
 | 
			
		||||
        assertThat(providersInfos.get(TwoFaProviderType.EMAIL).isDefault()).isFalse();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    public void testEnforceTwoFactorSetting() throws Exception {
 | 
			
		||||
        TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
 | 
			
		||||
        totpTwoFaProviderConfig.setIssuerName("tb");
 | 
			
		||||
 | 
			
		||||
        PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
 | 
			
		||||
        twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList()));
 | 
			
		||||
        twoFaSettings.setMinVerificationCodeSendPeriod(5);
 | 
			
		||||
        twoFaSettings.setTotalAllowedTimeForVerification(100);
 | 
			
		||||
        twoFaSettings.setEnforceTwoFa(true);
 | 
			
		||||
        twoFaSettings = twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
 | 
			
		||||
 | 
			
		||||
        JsonNode node = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class);
 | 
			
		||||
        assertNotNull(node.get("token").asText());
 | 
			
		||||
        assertNull(node.get("refreshToken"));
 | 
			
		||||
        assertEquals(node.get("scope").asText(), Authority.ENFORCE_MFA_TOKEN.name());
 | 
			
		||||
 | 
			
		||||
        this.token = node.get("token").asText();
 | 
			
		||||
        TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, totpTwoFaProviderConfig.getProviderType());
 | 
			
		||||
        String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build()
 | 
			
		||||
                .getQueryParams().getFirst("secret");
 | 
			
		||||
        String verificationCode = new Totp(secret).now();
 | 
			
		||||
        readResponse(doPost("/api/2fa/account/config?verificationCode=" + verificationCode, totpTwoFaAccountConfig).andExpect(status().isOk()), JsonNode.class);
 | 
			
		||||
 | 
			
		||||
        JwtPair tokenPair = readResponse(doPost("/api/auth/2fa/login").andExpect(status().isOk()), JwtPair.class);
 | 
			
		||||
        assertNotNull(tokenPair);
 | 
			
		||||
 | 
			
		||||
        this.token = tokenPair.getToken();
 | 
			
		||||
        this.refreshToken = tokenPair.getRefreshToken();
 | 
			
		||||
 | 
			
		||||
        doGet("/api/user/" + user.getId()).andExpect(status().isOk());
 | 
			
		||||
 | 
			
		||||
        twoFaSettings.setEnforceTwoFa(false);
 | 
			
		||||
        twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void logInWithPreVerificationToken(String username, String password) throws Exception {
 | 
			
		||||
        LoginRequest loginRequest = new LoginRequest(username, password);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -125,7 +125,7 @@ public class JwtTokenFactoryTest {
 | 
			
		||||
    public void testCreateAndParsePreVerificationJwtToken() {
 | 
			
		||||
        SecurityUser securityUser = createSecurityUser();
 | 
			
		||||
        int tokenLifetime = (int) TimeUnit.MINUTES.toSeconds(30);
 | 
			
		||||
        JwtToken preVerificationToken = tokenFactory.createPreVerificationToken(securityUser, tokenLifetime);
 | 
			
		||||
        JwtToken preVerificationToken = tokenFactory.createMfaToken(securityUser, Authority.PRE_VERIFICATION_TOKEN, tokenLifetime);
 | 
			
		||||
        checkExpirationTime(preVerificationToken, tokenLifetime);
 | 
			
		||||
 | 
			
		||||
        SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(preVerificationToken.getToken());
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,8 @@ public enum Authority {
 | 
			
		||||
    TENANT_ADMIN(1),
 | 
			
		||||
    CUSTOMER_USER(2),
 | 
			
		||||
    REFRESH_TOKEN(10),
 | 
			
		||||
    PRE_VERIFICATION_TOKEN(11);
 | 
			
		||||
    PRE_VERIFICATION_TOKEN(11),
 | 
			
		||||
    ENFORCE_MFA_TOKEN(12);
 | 
			
		||||
 | 
			
		||||
    private int code;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,7 @@ public class PlatformTwoFaSettings {
 | 
			
		||||
    @Min(value = 60)
 | 
			
		||||
    private Integer totalAllowedTimeForVerification;
 | 
			
		||||
 | 
			
		||||
    private boolean enforceTwoFa;
 | 
			
		||||
 | 
			
		||||
    public Optional<TwoFaProviderConfig> getProviderConfig(TwoFaProviderType providerType) {
 | 
			
		||||
        return Optional.ofNullable(providers)
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user