2FA: refactoring, tests
This commit is contained in:
parent
1ad769048c
commit
9eb03950fa
@ -283,11 +283,31 @@ public abstract class BaseController {
|
|||||||
@Getter
|
@Getter
|
||||||
protected boolean edgesEnabled;
|
protected boolean edgesEnabled;
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public void handleControllerException(Exception e, HttpServletResponse response) {
|
||||||
|
ThingsboardException thingsboardException = handleException(e);
|
||||||
|
handleThingsboardException(thingsboardException, response);
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(ThingsboardException.class)
|
@ExceptionHandler(ThingsboardException.class)
|
||||||
public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
|
public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
|
||||||
errorResponseHandler.handle(ex, response);
|
errorResponseHandler.handle(ex, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Exceptions that are not of {@link ThingsboardException} type
|
||||||
|
* are now caught and mapped to {@link ThingsboardException} by
|
||||||
|
* {@link ExceptionHandler} {@link BaseController#handleControllerException(Exception, HttpServletResponse)}
|
||||||
|
* which basically acts like the following boilerplate:
|
||||||
|
* {@code
|
||||||
|
* try {
|
||||||
|
* someExceptionThrowingMethod();
|
||||||
|
* } catch (Exception e) {
|
||||||
|
* throw handleException(e);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* */
|
||||||
|
@Deprecated
|
||||||
ThingsboardException handleException(Exception exception) {
|
ThingsboardException handleException(Exception exception) {
|
||||||
return handleException(exception, true);
|
return handleException(exception, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import org.thingsboard.common.util.JacksonUtil;
|
import org.thingsboard.common.util.JacksonUtil;
|
||||||
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
|
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
|
||||||
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
import org.thingsboard.server.common.data.exception.ThingsboardException;
|
||||||
|
import org.thingsboard.server.dao.service.ConstraintValidator;
|
||||||
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
|
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
|
||||||
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings;
|
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings;
|
||||||
import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig;
|
import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig;
|
||||||
@ -42,6 +43,9 @@ import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
|
|||||||
import javax.servlet.ServletOutputStream;
|
import javax.servlet.ServletOutputStream;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
// FIXME: Swagger documentation
|
||||||
|
// FIXME: tests for 2FA
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping("/api")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ -81,7 +85,7 @@ public class TwoFactorAuthController extends BaseController {
|
|||||||
MatrixToImageWriter.writeToStream(qr, "PNG", outputStream);
|
MatrixToImageWriter.writeToStream(qr, "PNG", outputStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response.setHeader("body", JacksonUtil.toString(config));
|
response.setHeader("config", JacksonUtil.toString(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/2fa/account/config/submit")
|
@PostMapping("/2fa/account/config/submit")
|
||||||
@ -91,6 +95,7 @@ public class TwoFactorAuthController extends BaseController {
|
|||||||
|
|
||||||
twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(),
|
twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(),
|
||||||
(provider, providerConfig) -> {
|
(provider, providerConfig) -> {
|
||||||
|
ConstraintValidator.validateFields(accountConfig);
|
||||||
provider.prepareVerificationCode(user, providerConfig, accountConfig);
|
provider.prepareVerificationCode(user, providerConfig, accountConfig);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,7 @@ import java.util.Collections;
|
|||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
@ -144,7 +145,7 @@ public class TwoFactorAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows({InterruptedException.class, ExecutionException.class})
|
||||||
public Optional<TwoFactorAuthSettings> getTwoFaSettings(TenantId tenantId) {
|
public Optional<TwoFactorAuthSettings> getTwoFaSettings(TenantId tenantId) {
|
||||||
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
|
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
|
||||||
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
|
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
|
||||||
@ -157,7 +158,7 @@ public class TwoFactorAuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows({InterruptedException.class, ExecutionException.class})
|
||||||
public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) {
|
public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) {
|
||||||
ConstraintValidator.validateFields(twoFactorAuthSettings);
|
ConstraintValidator.validateFields(twoFactorAuthSettings);
|
||||||
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
|
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
|
||||||
|
|||||||
@ -15,18 +15,16 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.service.security.auth.mfa.provider.impl;
|
package org.thingsboard.server.service.security.auth.mfa.provider.impl;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.Data;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
|
import org.springframework.cache.Cache;
|
||||||
|
import org.springframework.cache.CacheManager;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.thingsboard.rule.engine.api.SmsService;
|
import org.thingsboard.rule.engine.api.SmsService;
|
||||||
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
|
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
|
||||||
|
import org.thingsboard.server.common.data.CacheConstants;
|
||||||
import org.thingsboard.server.common.data.User;
|
import org.thingsboard.server.common.data.User;
|
||||||
import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery;
|
|
||||||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
|
|
||||||
import org.thingsboard.server.common.data.kv.StringDataEntry;
|
|
||||||
import org.thingsboard.server.dao.service.ConstraintValidator;
|
|
||||||
import org.thingsboard.server.dao.timeseries.TimeseriesService;
|
|
||||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||||
import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig;
|
import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig;
|
||||||
import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig;
|
import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig;
|
||||||
@ -34,16 +32,20 @@ import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthPr
|
|||||||
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
|
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
|
||||||
import org.thingsboard.server.service.security.model.SecurityUser;
|
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
|
||||||
@TbCoreComponent
|
@TbCoreComponent
|
||||||
public class SmsTwoFactorAuthProvider implements TwoFactorAuthProvider<SmsTwoFactorAuthProviderConfig, SmsTwoFactorAuthAccountConfig> {
|
public class SmsTwoFactorAuthProvider implements TwoFactorAuthProvider<SmsTwoFactorAuthProviderConfig, SmsTwoFactorAuthAccountConfig> {
|
||||||
|
|
||||||
private final SmsService smsService;
|
private final SmsService smsService;
|
||||||
private final TimeseriesService timeseriesService;
|
private final Cache verificationCodesCache;
|
||||||
|
|
||||||
|
public SmsTwoFactorAuthProvider(SmsService smsService, CacheManager cacheManager) {
|
||||||
|
this.smsService = smsService;
|
||||||
|
this.verificationCodesCache = cacheManager.getCache(CacheConstants.TWO_FA_VERIFICATION_CODES_CACHE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SmsTwoFactorAuthAccountConfig generateNewAccountConfig(User user, SmsTwoFactorAuthProviderConfig providerConfig) {
|
public SmsTwoFactorAuthAccountConfig generateNewAccountConfig(User user, SmsTwoFactorAuthProviderConfig providerConfig) {
|
||||||
@ -53,10 +55,8 @@ public class SmsTwoFactorAuthProvider implements TwoFactorAuthProvider<SmsTwoFac
|
|||||||
@Override
|
@Override
|
||||||
@SneakyThrows // fixme
|
@SneakyThrows // fixme
|
||||||
public void prepareVerificationCode(SecurityUser user, SmsTwoFactorAuthProviderConfig providerConfig, SmsTwoFactorAuthAccountConfig accountConfig) {
|
public void prepareVerificationCode(SecurityUser user, SmsTwoFactorAuthProviderConfig providerConfig, SmsTwoFactorAuthAccountConfig accountConfig) {
|
||||||
ConstraintValidator.validateFields(accountConfig);
|
|
||||||
|
|
||||||
String verificationCode = RandomStringUtils.randomNumeric(6);
|
String verificationCode = RandomStringUtils.randomNumeric(6);
|
||||||
saveVerificationCode(user, verificationCode);
|
verificationCodesCache.put(user.getSessionId(), new VerificationCode(System.currentTimeMillis(), verificationCode));
|
||||||
|
|
||||||
String phoneNumber = accountConfig.getPhoneNumber();
|
String phoneNumber = accountConfig.getPhoneNumber();
|
||||||
|
|
||||||
@ -71,40 +71,25 @@ public class SmsTwoFactorAuthProvider implements TwoFactorAuthProvider<SmsTwoFac
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean checkVerificationCode(SecurityUser user, String verificationCode, SmsTwoFactorAuthAccountConfig accountConfig) {
|
public boolean checkVerificationCode(SecurityUser user, String verificationCode, SmsTwoFactorAuthAccountConfig accountConfig) {
|
||||||
if (verificationCode.equals(getVerificationCode(user))) {
|
VerificationCode correctVerificationCode = verificationCodesCache.get(user.getSessionId(), VerificationCode.class);
|
||||||
removeVerificationCode(user);
|
if (correctVerificationCode != null && verificationCode.equals(correctVerificationCode.getValue())) {
|
||||||
|
verificationCodesCache.evict(user.getSessionId());
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
private void saveVerificationCode(SecurityUser user, String verificationCode) {
|
|
||||||
timeseriesService.save(user.getTenantId(), user.getId(),
|
|
||||||
new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("twoFaVerificationCode:" + user.getSessionId(), verificationCode))
|
|
||||||
).get();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
private String getVerificationCode(SecurityUser user) {
|
|
||||||
return timeseriesService.findLatest(user.getTenantId(), user.getId(),
|
|
||||||
Collections.singletonList("twoFaVerificationCode:" + user.getSessionId())).get().stream().findFirst()
|
|
||||||
.map(codeTs -> codeTs.getStrValue().get())
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeVerificationCode(SecurityUser user) {
|
|
||||||
timeseriesService.remove(user.getTenantId(), user.getId(), Collections.singletonList(
|
|
||||||
new BaseDeleteTsKvQuery("twoFaVerificationCode:" + user.getSessionId(), 0, System.currentTimeMillis())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TwoFactorAuthProviderType getType() {
|
public TwoFactorAuthProviderType getType() {
|
||||||
return TwoFactorAuthProviderType.SMS;
|
return TwoFactorAuthProviderType.SMS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Data
|
||||||
|
private static class VerificationCode {
|
||||||
|
private final long timestamp;
|
||||||
|
private final String value;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.security.web.WebAttributes;
|
import org.springframework.security.web.WebAttributes;
|
||||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.thingsboard.server.common.data.security.Authority;
|
||||||
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
|
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
|
||||||
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
|
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
|
||||||
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
|
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
|
||||||
@ -53,6 +54,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
|
|||||||
|
|
||||||
JwtTokenPair tokenPair;
|
JwtTokenPair tokenPair;
|
||||||
|
|
||||||
|
// fixme: check if this handler is not called when token is refreshed
|
||||||
Optional<TwoFactorAuthAccountConfig> twoFaAccountConfig = twoFactorAuthService.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId());
|
Optional<TwoFactorAuthAccountConfig> twoFaAccountConfig = twoFactorAuthService.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId());
|
||||||
if (twoFaAccountConfig.isPresent()) {
|
if (twoFaAccountConfig.isPresent()) {
|
||||||
try {
|
try {
|
||||||
@ -62,6 +64,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
|
|||||||
});
|
});
|
||||||
tokenPair = new JwtTokenPair();
|
tokenPair = new JwtTokenPair();
|
||||||
tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser).getToken());
|
tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser).getToken());
|
||||||
|
tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to process 2FA for user {}. Falling back to plain auth", securityUser.getId(), e);
|
log.error("Failed to process 2FA for user {}. Falling back to plain auth", securityUser.getId(), e);
|
||||||
tokenPair = tokenFactory.createTokenPair(securityUser);
|
tokenPair = tokenFactory.createTokenPair(securityUser);
|
||||||
|
|||||||
@ -20,10 +20,10 @@ import io.swagger.annotations.ApiModelProperty;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.thingsboard.server.common.data.security.Authority;
|
||||||
|
|
||||||
@ApiModel(value = "JWT Token Pair")
|
@ApiModel(value = "JWT Token Pair")
|
||||||
@Data
|
@Data
|
||||||
@AllArgsConstructor
|
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class JwtTokenPair {
|
public class JwtTokenPair {
|
||||||
|
|
||||||
@ -31,4 +31,12 @@ public class JwtTokenPair {
|
|||||||
private String token;
|
private String token;
|
||||||
@ApiModelProperty(position = 1, value = "The JWT Refresh Token. Used to get new JWT Access Token if old one has expired.", example = "AAB254FF67D..")
|
@ApiModelProperty(position = 1, value = "The JWT Refresh Token. Used to get new JWT Access Token if old one has expired.", example = "AAB254FF67D..")
|
||||||
private String refreshToken;
|
private String refreshToken;
|
||||||
|
|
||||||
|
private Authority scope;
|
||||||
|
|
||||||
|
public JwtTokenPair(String token, String refreshToken) {
|
||||||
|
this.token = token;
|
||||||
|
this.refreshToken = refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -428,8 +428,8 @@ caffeine:
|
|||||||
timeToLiveInMinutes: "${CACHE_SPECS_EDGES_TTL:1440}"
|
timeToLiveInMinutes: "${CACHE_SPECS_EDGES_TTL:1440}"
|
||||||
maxSize: "${CACHE_SPECS_EDGES_MAX_SIZE:10000}"
|
maxSize: "${CACHE_SPECS_EDGES_MAX_SIZE:10000}"
|
||||||
twoFaVerificationCodes:
|
twoFaVerificationCodes:
|
||||||
timeToLiveInMinutes: "1"
|
timeToLiveInMinutes: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_TTL:1}"
|
||||||
maxSize: "100000"
|
maxSize: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_MAX_SIZE:100000}"
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
# standalone or cluster
|
# standalone or cluster
|
||||||
|
|||||||
@ -584,6 +584,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
|
|||||||
return mapper.readerFor(type).readValue(content);
|
return mapper.readerFor(type).readValue(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected String getErrorMessage(ResultActions result) throws Exception {
|
||||||
|
return readResponse(result, JsonNode.class).get("message").asText();
|
||||||
|
}
|
||||||
|
|
||||||
public class IdComparator<D extends HasId> implements Comparator<D> {
|
public class IdComparator<D extends HasId> implements Comparator<D> {
|
||||||
@Override
|
@Override
|
||||||
public int compare(D o1, D o2) {
|
public int compare(D o1, D o2) {
|
||||||
|
|||||||
@ -0,0 +1,256 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.controller;
|
||||||
|
|
||||||
|
import org.jboss.aerogear.security.otp.Totp;
|
||||||
|
import org.jboss.aerogear.security.otp.api.Base32;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||||
|
import org.springframework.web.util.UriComponents;
|
||||||
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
import org.thingsboard.rule.engine.api.SmsService;
|
||||||
|
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings;
|
||||||
|
import org.thingsboard.server.service.security.auth.mfa.config.account.SmsTwoFactorAuthAccountConfig;
|
||||||
|
import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig;
|
||||||
|
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
|
||||||
|
import org.thingsboard.server.service.security.auth.mfa.config.provider.SmsTwoFactorAuthProviderConfig;
|
||||||
|
import org.thingsboard.server.service.security.auth.mfa.config.provider.TotpTwoFactorAuthProviderConfig;
|
||||||
|
import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig;
|
||||||
|
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
|
||||||
|
import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFactorAuthProvider;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
public abstract class TwoFactorAuthTest extends AbstractControllerTest {
|
||||||
|
|
||||||
|
@SpyBean
|
||||||
|
private TotpTwoFactorAuthProvider totpTwoFactorAuthProvider;
|
||||||
|
@MockBean
|
||||||
|
private SmsService smsService;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeEach() throws Exception {
|
||||||
|
loginSysAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSaveTwoFaSettings() throws Exception {
|
||||||
|
loginSysAdmin();
|
||||||
|
testSaveTestTwoFaSettings();
|
||||||
|
|
||||||
|
loginTenantAdmin();
|
||||||
|
testSaveTestTwoFaSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testSaveTestTwoFaSettings() throws Exception {
|
||||||
|
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig();
|
||||||
|
totpTwoFaProviderConfig.setIssuerName("tb");
|
||||||
|
SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig();
|
||||||
|
smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${verificationCode}");
|
||||||
|
|
||||||
|
saveProvidersConfigs(totpTwoFaProviderConfig, smsTwoFaProviderConfig);
|
||||||
|
|
||||||
|
TwoFactorAuthSettings savedTwoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), TwoFactorAuthSettings.class);
|
||||||
|
|
||||||
|
assertThat(savedTwoFaSettings.getProviders()).hasSize(2);
|
||||||
|
assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSaveTotpTwoFaProviderConfig_validationError() throws Exception {
|
||||||
|
TotpTwoFactorAuthProviderConfig invalidTotpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig();
|
||||||
|
invalidTotpTwoFaProviderConfig.setIssuerName(" ");
|
||||||
|
|
||||||
|
String errorResponse = saveTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig);
|
||||||
|
assertThat(errorResponse).containsIgnoringCase("issuer name must not be blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSaveSmsTwoFaProviderConfig_validationError() throws Exception {
|
||||||
|
SmsTwoFactorAuthProviderConfig invalidSmsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig();
|
||||||
|
invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate("does not contain verification code");
|
||||||
|
|
||||||
|
String errorResponse = saveTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig);
|
||||||
|
assertThat(errorResponse).containsIgnoringCase("must contain verification code");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String saveTwoFaSettingsAndGetError(TwoFactorAuthProviderConfig invalidTwoFaProviderConfig) throws Exception {
|
||||||
|
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings();
|
||||||
|
twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig));
|
||||||
|
|
||||||
|
return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
|
||||||
|
.andExpect(status().isBadRequest()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSaveTwoFaAccountConfig_providerNotConfigured() throws Exception {
|
||||||
|
configureSmsTwoFaProvider("${verificationCode}");
|
||||||
|
|
||||||
|
loginTenantAdmin();
|
||||||
|
|
||||||
|
TwoFactorAuthProviderType notConfiguredProviderType = TwoFactorAuthProviderType.TOTP;
|
||||||
|
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=" + notConfiguredProviderType)
|
||||||
|
.andExpect(status().isBadRequest()));
|
||||||
|
assertThat(errorMessage).containsIgnoringCase("provider is not configured");
|
||||||
|
|
||||||
|
TotpTwoFactorAuthAccountConfig notConfiguredProviderAccountConfig = new TotpTwoFactorAuthAccountConfig();
|
||||||
|
notConfiguredProviderAccountConfig.setAuthUrl("aba");
|
||||||
|
errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", notConfiguredProviderAccountConfig));
|
||||||
|
assertThat(errorMessage).containsIgnoringCase("provider is not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGenerateTotpTwoFaAccountConfig() throws Exception {
|
||||||
|
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
|
||||||
|
|
||||||
|
loginTenantAdmin();
|
||||||
|
|
||||||
|
assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class)).isNullOrEmpty();
|
||||||
|
generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSubmitTotpTwoFaAccountConfig() throws Exception {
|
||||||
|
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
|
||||||
|
|
||||||
|
loginTenantAdmin();
|
||||||
|
|
||||||
|
TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
|
||||||
|
doPost("/api/2fa/account/config/submit", generatedTotpTwoFaAccountConfig).andExpect(status().isOk());
|
||||||
|
verify(totpTwoFactorAuthProvider).prepareVerificationCode(argThat(user -> user.getEmail().equals(TENANT_ADMIN_EMAIL)),
|
||||||
|
eq(totpTwoFaProviderConfig), eq(generatedTotpTwoFaAccountConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifyAndSaveTotpTwoFaAccountConfig() throws Exception {
|
||||||
|
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
|
||||||
|
|
||||||
|
loginTenantAdmin();
|
||||||
|
|
||||||
|
TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
|
||||||
|
|
||||||
|
String secret = UriComponentsBuilder.fromUriString(generatedTotpTwoFaAccountConfig.getAuthUrl()).build()
|
||||||
|
.getQueryParams().getFirst("secret");
|
||||||
|
String correctVerificationCode = new Totp(secret).now();
|
||||||
|
|
||||||
|
doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, generatedTotpTwoFaAccountConfig)
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
|
||||||
|
TwoFactorAuthAccountConfig twoFaAccountConfig = readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), TwoFactorAuthAccountConfig.class);
|
||||||
|
assertThat(twoFaAccountConfig).isEqualTo(generatedTotpTwoFaAccountConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVerifyAndSaveTotpTwoFaAccountConfig_incorrectVerificationCode() throws Exception {
|
||||||
|
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
|
||||||
|
|
||||||
|
loginTenantAdmin();
|
||||||
|
|
||||||
|
TotpTwoFactorAuthAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
|
||||||
|
|
||||||
|
String incorrectVerificationCode = "100000";
|
||||||
|
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + incorrectVerificationCode, generatedTotpTwoFaAccountConfig)
|
||||||
|
.andExpect(status().isBadRequest()));
|
||||||
|
|
||||||
|
assertThat(errorMessage).containsIgnoringCase("verification code is incorrect");
|
||||||
|
}
|
||||||
|
|
||||||
|
private TotpTwoFactorAuthAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig) throws Exception {
|
||||||
|
TwoFactorAuthAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP")
|
||||||
|
.andExpect(status().isOk()), TwoFactorAuthAccountConfig.class);
|
||||||
|
assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFactorAuthAccountConfig.class);
|
||||||
|
|
||||||
|
assertThat(((TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> {
|
||||||
|
UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build();
|
||||||
|
assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth");
|
||||||
|
assertThat(otpAuthUrl.getHost()).isEqualTo("totp");
|
||||||
|
assertThat(otpAuthUrl.getQueryParams().getFirst("issuer")).isEqualTo(totpTwoFaProviderConfig.getIssuerName());
|
||||||
|
assertThat(otpAuthUrl.getPath()).isEqualTo("/%s:%s", totpTwoFaProviderConfig.getIssuerName(), TENANT_ADMIN_EMAIL);
|
||||||
|
assertThat(otpAuthUrl.getQueryParams().getFirst("secret")).satisfies(secretKey -> {
|
||||||
|
assertDoesNotThrow(() -> Base32.decode(secretKey));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return (TotpTwoFactorAuthAccountConfig) generatedTwoFaAccountConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception {
|
||||||
|
testVerifyAndSaveTotpTwoFaAccountConfig();
|
||||||
|
assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()),
|
||||||
|
TotpTwoFactorAuthAccountConfig.class)).isNotNull();
|
||||||
|
|
||||||
|
loginSysAdmin();
|
||||||
|
|
||||||
|
saveProvidersConfigs();
|
||||||
|
|
||||||
|
assertThat(readResponse(doGet("/api/2fa/account/config").andExpect(status().isOk()), String.class))
|
||||||
|
.isNullOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
// public void testSubmitSmsTwoFaAccountConfig() throws Exception {
|
||||||
|
// String verificationMessageTemplate = "Here is your verification code: ${verificationCode}";
|
||||||
|
// SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = configureSmsTwoFaProvider(verificationMessageTemplate);
|
||||||
|
//
|
||||||
|
// SmsTwoFactorAuthAccountConfig smsTwoFaAccountConfig = new SmsTwoFactorAuthAccountConfig();
|
||||||
|
// smsTwoFaAccountConfig.setPhoneNumber("+38054159785");
|
||||||
|
//
|
||||||
|
// String verificationCode = ""; ?
|
||||||
|
//
|
||||||
|
// verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> {
|
||||||
|
// return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber())
|
||||||
|
// }), eq("Here is your verification code: " + verificationCode));
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private TotpTwoFactorAuthProviderConfig configureTotpTwoFaProvider() throws Exception {
|
||||||
|
TotpTwoFactorAuthProviderConfig totpTwoFaProviderConfig = new TotpTwoFactorAuthProviderConfig();
|
||||||
|
totpTwoFaProviderConfig.setIssuerName("tb");
|
||||||
|
|
||||||
|
saveProvidersConfigs(totpTwoFaProviderConfig);
|
||||||
|
return totpTwoFaProviderConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SmsTwoFactorAuthProviderConfig configureSmsTwoFaProvider(String verificationMessageTemplate) throws Exception {
|
||||||
|
SmsTwoFactorAuthProviderConfig smsTwoFaProviderConfig = new SmsTwoFactorAuthProviderConfig();
|
||||||
|
smsTwoFaProviderConfig.setSmsVerificationMessageTemplate(verificationMessageTemplate);
|
||||||
|
|
||||||
|
saveProvidersConfigs(smsTwoFaProviderConfig);
|
||||||
|
return smsTwoFaProviderConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveProvidersConfigs(TwoFactorAuthProviderConfig... providerConfigs) throws Exception {
|
||||||
|
TwoFactorAuthSettings twoFaSettings = new TwoFactorAuthSettings();
|
||||||
|
twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList()));
|
||||||
|
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.controller.sql;
|
||||||
|
|
||||||
|
import org.thingsboard.server.controller.TwoFactorAuthTest;
|
||||||
|
import org.thingsboard.server.dao.service.DaoSqlTest;
|
||||||
|
|
||||||
|
@DaoSqlTest
|
||||||
|
public class TwoFactorAuthSqlTest extends TwoFactorAuthTest {
|
||||||
|
}
|
||||||
@ -31,4 +31,5 @@ public class CacheConstants {
|
|||||||
public static final String TOKEN_OUTDATAGE_TIME_CACHE = "tokensOutdatageTime";
|
public static final String TOKEN_OUTDATAGE_TIME_CACHE = "tokensOutdatageTime";
|
||||||
public static final String OTA_PACKAGE_CACHE = "otaPackages";
|
public static final String OTA_PACKAGE_CACHE = "otaPackages";
|
||||||
public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData";
|
public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData";
|
||||||
|
public static final String TWO_FA_VERIFICATION_CODES_CACHE = "twoFaVerificationCodes";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import org.hibernate.validator.HibernateValidatorConfiguration;
|
|||||||
import org.hibernate.validator.cfg.ConstraintMapping;
|
import org.hibernate.validator.cfg.ConstraintMapping;
|
||||||
import org.thingsboard.server.common.data.validation.Length;
|
import org.thingsboard.server.common.data.validation.Length;
|
||||||
import org.thingsboard.server.common.data.validation.NoXss;
|
import org.thingsboard.server.common.data.validation.NoXss;
|
||||||
|
import org.thingsboard.server.dao.exception.DataValidationException;
|
||||||
|
|
||||||
import javax.validation.ConstraintViolation;
|
import javax.validation.ConstraintViolation;
|
||||||
import javax.validation.Validation;
|
import javax.validation.Validation;
|
||||||
@ -46,7 +47,7 @@ public class ConstraintValidator {
|
|||||||
.distinct()
|
.distinct()
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
if (!validationErrors.isEmpty()) {
|
if (!validationErrors.isEmpty()) {
|
||||||
throw new ValidationException(String.join(", ", validationErrors));
|
throw new DataValidationException("Validation error: " + String.join(", ", validationErrors));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -675,7 +675,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
|
|||||||
firmwareInfo.setUrl(URL);
|
firmwareInfo.setUrl(URL);
|
||||||
firmwareInfo.setTenantId(tenantId);
|
firmwareInfo.setTenantId(tenantId);
|
||||||
|
|
||||||
thrown.expect(ValidationException.class);
|
thrown.expect(DataValidationException.class);
|
||||||
thrown.expectMessage("length of title must be equal or less than 255");
|
thrown.expectMessage("length of title must be equal or less than 255");
|
||||||
|
|
||||||
otaPackageService.saveOtaPackageInfo(firmwareInfo, true);
|
otaPackageService.saveOtaPackageInfo(firmwareInfo, true);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user