Browse Source

TOTP and SMS 2FA providers; 2FA set-up API

pull/6235/head
Viacheslav Klimov 4 years ago
parent
commit
e2c9a5ffdf
  1. 16
      application/pom.xml
  2. 128
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  3. 179
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java
  4. 40
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java
  5. 34
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java
  6. 34
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java
  7. 37
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java
  8. 36
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java
  9. 34
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java
  10. 37
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java
  11. 34
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java
  12. 21
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java
  13. 110
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java
  14. 73
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java
  15. 21
      common/util/src/main/java/org/thingsboard/common/util/TripleFunction.java
  16. 2
      dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java
  17. 6
      pom.xml
  18. 8
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java

16
application/pom.xml

@ -337,6 +337,22 @@
<artifactId>Java-WebSocket</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.aerogear</groupId>
<artifactId>aerogear-otp-java</artifactId>
</dependency>
<!-- TMP -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.0</version>
</dependency>
<!-- TMP -->
</dependencies>
<build>

128
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java

@ -0,0 +1,128 @@
/**
* 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 com.google.zxing.BarcodeFormat;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
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.account.TotpTwoFactorAuthAccountConfig;
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
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.token.JwtTokenFactory;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class TwoFactorAuthController extends BaseController {
private final TwoFactorAuthService twoFactorAuthService;
private final JwtTokenFactory tokenFactory;
@GetMapping("/2fa/account/config")
@PreAuthorize("isAuthenticated()")
public TwoFactorAuthAccountConfig getTwoFactorAuthAccountConfig() throws ThingsboardException {
SecurityUser user = getCurrentUser();
return twoFactorAuthService.getTwoFaAccountConfig(user.getTenantId(), user.getId()).orElse(null);
}
@PostMapping("/2fa/account/config/generate")
@PreAuthorize("isAuthenticated()")
public TwoFactorAuthAccountConfig generateTwoFactorAuthAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws ThingsboardException {
SecurityUser user = getCurrentUser();
return twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), providerType,
(provider, providerConfig) -> {
return provider.generateNewAccountConfig(user, providerConfig);
});
}
// temporary endpoint for testing purposes
@PostMapping("/2fa/account/config/generate/qr")
@PreAuthorize("isAuthenticated()")
public void generateTwoFactorAuthAccountConfigWithQr(@RequestParam TwoFactorAuthProviderType providerType, HttpServletResponse response) throws Exception {
TwoFactorAuthAccountConfig config = generateTwoFactorAuthAccountConfig(providerType);
if (providerType == TwoFactorAuthProviderType.TOTP) {
BitMatrix qr = new QRCodeWriter().encode(((TotpTwoFactorAuthAccountConfig) config).getAuthUrl(), BarcodeFormat.QR_CODE, 200, 200);
try (ServletOutputStream outputStream = response.getOutputStream()) {
MatrixToImageWriter.writeToStream(qr, "PNG", outputStream);
}
}
response.setHeader("body", JacksonUtil.toString(config));
}
@PostMapping("/2fa/account/config/submit")
@PreAuthorize("isAuthenticated()")
public void submitTwoFactorAuthAccountConfig(@RequestBody TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException {
SecurityUser user = getCurrentUser();
twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(),
(provider, providerConfig) -> {
provider.prepareVerificationCode(user, providerConfig, accountConfig);
});
}
@PostMapping("/2fa/account/config")
@PreAuthorize("isAuthenticated()")
public void verifyAndSaveTwoFactorAuthAccountConfig(@RequestBody TwoFactorAuthAccountConfig accountConfig,
@RequestParam String verificationCode) throws ThingsboardException {
SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(),
(provider, providerConfig) -> {
return provider.checkVerificationCode(user, verificationCode, accountConfig);
});
if (verificationSuccess) {
twoFactorAuthService.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
} else {
throw new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.INVALID_ARGUMENTS);
}
}
@GetMapping("/2fa/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public TwoFactorAuthSettings getTwoFactorAuthSettings() throws ThingsboardException {
return twoFactorAuthService.getTwoFaSettings(getTenantId()).orElse(null);
}
@PostMapping("/2fa/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
public void saveTwoFactorAuthSettings(@RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException {
twoFactorAuthService.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings);
}
}

179
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java

@ -0,0 +1,179 @@
/**
* 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.service.security.auth.mfa;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.TripleFunction;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.User;
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.id.UserId;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings;
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
@Service
@RequiredArgsConstructor
public class TwoFactorAuthService {
private final UserService userService;
private final AdminSettingsService adminSettingsService;
private final AttributesService attributesService;
private final Map<TwoFactorAuthProviderType, TwoFactorAuthProvider<?, ?>> providers = new EnumMap<>(TwoFactorAuthProviderType.class);
protected static final String TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY = "twoFaConfig";
protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings";
@Autowired
private void setProviders(Collection<TwoFactorAuthProvider<?, ?>> providers) {
providers.forEach(provider -> {
this.providers.put(provider.getType(), provider);
});
}
private <A extends TwoFactorAuthAccountConfig, C extends TwoFactorAuthProviderConfig> Optional<TwoFactorAuthProvider<C, A>> getTwoFaProvider(TwoFactorAuthProviderType providerType) {
return Optional.of((TwoFactorAuthProvider<C, A>) providers.get(providerType));
}
private <C extends TwoFactorAuthProviderConfig> Optional<C> getTwoFaProviderConfig(TenantId tenantId, TwoFactorAuthProviderType providerType) {
return getTwoFaSettings(tenantId)
.flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType))
.map(providerConfig -> (C) providerConfig);
}
public <R> R processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, BiFunction<TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig>, TwoFactorAuthProviderConfig, R> function) throws ThingsboardException {
TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(tenantId, providerType)
.orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS));
TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig> provider = getTwoFaProvider(providerType)
.orElseThrow(() -> new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.ITEM_NOT_FOUND));
return function.apply(provider, providerConfig);
}
public void processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, BiConsumer<TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig>, TwoFactorAuthProviderConfig> function) throws ThingsboardException {
processByTwoFaProvider(tenantId, providerType, (provider, providerConfig) -> {
function.accept(provider, providerConfig);
return null;
});
}
public <R> R processByTwoFaProvider(TenantId tenantId, UserId userId, TripleFunction<TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig>, TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig, R> function) throws ThingsboardException {
TwoFactorAuthAccountConfig accountConfig = getTwoFaAccountConfig(tenantId, userId)
.orElseThrow(() -> new ThingsboardException("2FA is not configured for user", ThingsboardErrorCode.BAD_REQUEST_PARAMS));
TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(tenantId, accountConfig.getProviderType())
.orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS));
TwoFactorAuthProvider<TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig> provider = getTwoFaProvider(accountConfig.getProviderType())
.orElseThrow(() -> new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.ITEM_NOT_FOUND));
return function.apply(provider, providerConfig, accountConfig);
}
public Optional<TwoFactorAuthAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId) {
User user = userService.findUserById(tenantId, userId);
return Optional.ofNullable(user.getAdditionalInfo())
.flatMap(additionalInfo -> Optional.ofNullable(additionalInfo.get(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY)).filter(jsonNode -> !jsonNode.isNull()))
.map(jsonNode -> JacksonUtil.treeToValue(jsonNode, TwoFactorAuthAccountConfig.class))
.filter(twoFactorAuthAccountConfig -> {
return getTwoFaProviderConfig(tenantId, twoFactorAuthAccountConfig.getProviderType()).isPresent();
});
}
public void saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException {
ConstraintValidator.validateFields(accountConfig);
getTwoFaProviderConfig(tenantId, accountConfig.getProviderType())
.orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS));
User user = userService.findUserById(tenantId, userId);
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo())
.orElseGet(JacksonUtil::newObjectNode);
additionalInfo.set(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY, JacksonUtil.valueToTree(accountConfig));
user.setAdditionalInfo(additionalInfo);
userService.saveUser(user);
}
public void deleteTwoFaAccountConfig(TenantId tenantId, UserId userId) {
User user = userService.findUserById(tenantId, userId);
ObjectNode additionalInfo = (ObjectNode) Optional.ofNullable(user.getAdditionalInfo())
.orElseGet(JacksonUtil::newObjectNode);
additionalInfo.remove(TWO_FACTOR_AUTH_ACCOUNT_CONFIG_KEY);
user.setAdditionalInfo(additionalInfo);
userService.saveUser(user);
}
@SneakyThrows
public Optional<TwoFactorAuthSettings> getTwoFaSettings(TenantId tenantId) {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), TwoFactorAuthSettings.class));
} else {
return attributesService.find(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, TWO_FACTOR_AUTH_SETTINGS_KEY).get()
.map(adminSettingsAttribute -> JacksonUtil.fromString(adminSettingsAttribute.getJsonValue().get(), TwoFactorAuthSettings.class))
.filter(tenantTwoFactorAuthSettings -> !tenantTwoFactorAuthSettings.isUseSystemTwoFactorAuthSettings())
.or(() -> getTwoFaSettings(TenantId.SYS_TENANT_ID));
}
}
@SneakyThrows
public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) {
ConstraintValidator.validateFields(twoFactorAuthSettings);
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
.orElseGet(() -> {
AdminSettings newSettings = new AdminSettings();
newSettings.setKey(TWO_FACTOR_AUTH_SETTINGS_KEY);
return newSettings;
});
settings.setJsonValue(JacksonUtil.valueToTree(twoFactorAuthSettings));
adminSettingsService.saveAdminSettings(tenantId, settings);
} else {
attributesService.save(TenantId.SYS_TENANT_ID, tenantId, DataConstants.SERVER_SCOPE, Collections.singletonList(
new BaseAttributeKvEntry(new JsonDataEntry(TWO_FACTOR_AUTH_SETTINGS_KEY, JacksonUtil.toString(twoFactorAuthSettings)), System.currentTimeMillis())
)).get();
}
}
}

40
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFactorAuthSettings.java

@ -0,0 +1,40 @@
/**
* 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.service.security.auth.mfa.config;
import lombok.Data;
import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import javax.validation.Valid;
import java.util.List;
import java.util.Optional;
@Data
public class TwoFactorAuthSettings {
private boolean useSystemTwoFactorAuthSettings;
@Valid
private List<TwoFactorAuthProviderConfig> providers;
public Optional<TwoFactorAuthProviderConfig> getProviderConfig(TwoFactorAuthProviderType providerType) {
return Optional.ofNullable(providers)
.flatMap(providersConfigs -> providersConfigs.stream()
.filter(providerConfig -> providerConfig.getProviderType() == providerType)
.findFirst());
}
}

34
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/SmsTwoFactorAuthAccountConfig.java

@ -0,0 +1,34 @@
/**
* 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.service.security.auth.mfa.config.account;
import lombok.Data;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import javax.validation.constraints.NotBlank;
@Data
public class SmsTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig {
@NotBlank
private String phoneNumber;
@Override
public TwoFactorAuthProviderType getProviderType() {
return TwoFactorAuthProviderType.SMS;
}
}

34
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java

@ -0,0 +1,34 @@
/**
* 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.service.security.auth.mfa.config.account;
import lombok.Data;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import javax.validation.constraints.NotBlank;
@Data
public class TotpTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig {
@NotBlank
private String authUrl;
@Override
public TwoFactorAuthProviderType getProviderType() {
return TwoFactorAuthProviderType.TOTP;
}
}

37
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TwoFactorAuthAccountConfig.java

@ -0,0 +1,37 @@
/**
* 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.service.security.auth.mfa.config.account;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "providerType")
@JsonSubTypes({
@JsonSubTypes.Type(value = TotpTwoFactorAuthAccountConfig.class, name = "TOTP"),
@JsonSubTypes.Type(value = SmsTwoFactorAuthAccountConfig.class, name = "SMS"),
})
public interface TwoFactorAuthAccountConfig {
@JsonIgnore
TwoFactorAuthProviderType getProviderType();
}

36
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java

@ -0,0 +1,36 @@
/**
* 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.service.security.auth.mfa.config.provider;
import lombok.Data;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@Data
public class SmsTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig {
@NotBlank
@Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "Template must contain verification code")
private String smsVerificationMessageTemplate;
@Override
public TwoFactorAuthProviderType getProviderType() {
return TwoFactorAuthProviderType.SMS;
}
}

34
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java

@ -0,0 +1,34 @@
/**
* 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.service.security.auth.mfa.config.provider;
import lombok.Data;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import javax.validation.constraints.NotBlank;
@Data
public class TotpTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig {
@NotBlank(message = "Issuer name must not be blank")
private String issuerName;
@Override
public TwoFactorAuthProviderType getProviderType() {
return TwoFactorAuthProviderType.TOTP;
}
}

37
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TwoFactorAuthProviderConfig.java

@ -0,0 +1,37 @@
/**
* 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.service.security.auth.mfa.config.provider;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "providerType")
@JsonSubTypes({
@JsonSubTypes.Type(value = TotpTwoFactorAuthProviderConfig.class, name = "TOTP"),
@JsonSubTypes.Type(value = SmsTwoFactorAuthProviderConfig.class, name = "SMS"),
})
public interface TwoFactorAuthProviderConfig {
@JsonIgnore
TwoFactorAuthProviderType getProviderType();
}

34
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java

@ -0,0 +1,34 @@
/**
* 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.service.security.auth.mfa.provider;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig;
import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface TwoFactorAuthProvider<C extends TwoFactorAuthProviderConfig, A extends TwoFactorAuthAccountConfig> {
A generateNewAccountConfig(User user, C providerConfig);
default void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) {}
boolean checkVerificationCode(SecurityUser user, String verificationCode, A accountConfig);
TwoFactorAuthProviderType getType();
}

21
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProviderType.java

@ -0,0 +1,21 @@
/**
* 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.service.security.auth.mfa.provider;
public enum TwoFactorAuthProviderType {
TOTP,
SMS
}

110
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFactorAuthProvider.java

@ -0,0 +1,110 @@
/**
* 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.service.security.auth.mfa.provider.impl;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
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.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.provider.TwoFactorAuthProvider;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Collections;
import java.util.Map;
@Service
@RequiredArgsConstructor
@TbCoreComponent
public class SmsTwoFactorAuthProvider implements TwoFactorAuthProvider<SmsTwoFactorAuthProviderConfig, SmsTwoFactorAuthAccountConfig> {
private final SmsService smsService;
private final TimeseriesService timeseriesService;
@Override
public SmsTwoFactorAuthAccountConfig generateNewAccountConfig(User user, SmsTwoFactorAuthProviderConfig providerConfig) {
return new SmsTwoFactorAuthAccountConfig();
}
@Override
@SneakyThrows // fixme
public void prepareVerificationCode(SecurityUser user, SmsTwoFactorAuthProviderConfig providerConfig, SmsTwoFactorAuthAccountConfig accountConfig) {
ConstraintValidator.validateFields(accountConfig);
String verificationCode = RandomStringUtils.randomNumeric(6);
saveVerificationCode(user, verificationCode);
String phoneNumber = accountConfig.getPhoneNumber();
Map<String, String> data = Map.of(
"verificationCode", verificationCode,
"userEmail", user.getEmail()
);
String message = TbNodeUtils.processTemplate(providerConfig.getSmsVerificationMessageTemplate(), data);
smsService.sendSms(user.getTenantId(), user.getCustomerId(), new String[]{phoneNumber}, message);
}
@Override
public boolean checkVerificationCode(SecurityUser user, String verificationCode, SmsTwoFactorAuthAccountConfig accountConfig) {
if (verificationCode.equals(getVerificationCode(user))) {
removeVerificationCode(user);
return true;
} else {
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
public TwoFactorAuthProviderType getType() {
return TwoFactorAuthProviderType.SMS;
}
}

73
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFactorAuthProvider.java

@ -0,0 +1,73 @@
/**
* 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.service.security.auth.mfa.provider.impl;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.lang3.RandomUtils;
import org.apache.http.client.utils.URIBuilder;
import org.jboss.aerogear.security.otp.Totp;
import org.jboss.aerogear.security.otp.api.Base32;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig;
import org.thingsboard.server.service.security.auth.mfa.config.provider.TotpTwoFactorAuthProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProvider;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType;
import org.thingsboard.server.service.security.model.SecurityUser;
@Service
@RequiredArgsConstructor
@TbCoreComponent
public class TotpTwoFactorAuthProvider implements TwoFactorAuthProvider<TotpTwoFactorAuthProviderConfig, TotpTwoFactorAuthAccountConfig> {
@Override
public final TotpTwoFactorAuthAccountConfig generateNewAccountConfig(User user, TotpTwoFactorAuthProviderConfig providerConfig) {
TotpTwoFactorAuthAccountConfig config = new TotpTwoFactorAuthAccountConfig();
String secretKey = generateSecretKey();
config.setAuthUrl(getTotpAuthUrl(user, secretKey, providerConfig));
return config;
}
@Override
public final boolean checkVerificationCode(SecurityUser user, String verificationCode, TotpTwoFactorAuthAccountConfig accountConfig) {
String secretKey = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build().getQueryParams().getFirst("secret");
return new Totp(secretKey).verify(verificationCode);
}
@SneakyThrows
private String getTotpAuthUrl(User user, String secretKey, TotpTwoFactorAuthProviderConfig providerConfig) {
URIBuilder uri = new URIBuilder()
.setScheme("otpauth")
.setHost("totp")
.setParameter("issuer", providerConfig.getIssuerName())
.setPath("/" + providerConfig.getIssuerName() + ":" + user.getEmail())
.setParameter("secret", secretKey);
return uri.build().toASCIIString();
}
private String generateSecretKey() {
return Base32.encode(RandomUtils.nextBytes(20));
}
@Override
public TwoFactorAuthProviderType getType() {
return TwoFactorAuthProviderType.TOTP;
}
}

21
common/util/src/main/java/org/thingsboard/common/util/TripleFunction.java

@ -0,0 +1,21 @@
/**
* 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.common.util;
@FunctionalInterface
public interface TripleFunction<A, B, C, R> {
R apply(A a, B b, C c);
}

2
dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java

@ -46,7 +46,7 @@ public class ConstraintValidator {
.distinct()
.collect(Collectors.toList());
if (!validationErrors.isEmpty()) {
throw new ValidationException("Validation error: " + String.join(", ", validationErrors));
throw new ValidationException(String.join(", ", validationErrors));
}
}

6
pom.xml

@ -132,6 +132,7 @@
<!-- BLACKBOX TEST SCOPE -->
<testcontainers.version>1.16.0</testcontainers.version>
<zeroturnaround.version>1.12</zeroturnaround.version>
<aerogear-otp.version>1.0.0</aerogear-otp.version>
</properties>
<modules>
@ -1872,6 +1873,11 @@
<version>${zeroturnaround.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.aerogear</groupId>
<artifactId>aerogear-otp-java</artifactId>
<version>${aerogear-otp.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

8
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java

@ -101,6 +101,14 @@ public class TbNodeUtils {
return result;
}
public static String processTemplate(String template, Map<String, String> data) {
String result = template;
for (Map.Entry<String, String> kv : data.entrySet()) {
result = processVar(result, kv.getKey(), kv.getValue());
}
return result;
}
private static String processVar(String pattern, String key, String val) {
return pattern.replace(formatMetadataVarTemplate(key), val);
}

Loading…
Cancel
Save