diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java
index 274630cc39..dddb011fd5 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java
@@ -36,10 +36,10 @@ public class DefaultJwtSettingsValidator implements JwtSettingsValidator {
if (StringUtils.isEmpty(jwtSettings.getTokenIssuer())) {
throw new DataValidationException("JWT token issuer should be specified!");
}
- if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(15)) {
+ if (Optional.ofNullable(jwtSettings.getRefreshTokenExpTime()).orElse(0) < TimeUnit.MINUTES.toSeconds(15)) {
throw new DataValidationException("JWT refresh token expiration time should be at least 15 minutes!");
}
- if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) <= TimeUnit.MINUTES.toSeconds(1)) {
+ if (Optional.ofNullable(jwtSettings.getTokenExpirationTime()).orElse(0) < TimeUnit.MINUTES.toSeconds(1)) {
throw new DataValidationException("JWT token expiration time should be at least 1 minute!");
}
if (jwtSettings.getTokenExpirationTime() >= jwtSettings.getRefreshTokenExpTime()) {
diff --git a/pom.xml b/pom.xml
index 49f30df835..15e27d2ebb 100755
--- a/pom.xml
+++ b/pom.xml
@@ -67,6 +67,7 @@
2.13.4
2.13.4.2
1.3.4
+ 4.2.1
2.2.6
3.0.0
2.0.0-M5
@@ -1429,6 +1430,11 @@
classmate
${fasterxml-classmate.version}
+
+ com.auth0
+ java-jwt
+ ${auth0-jwt.version}
+
com.github.fge
json-schema-validator
diff --git a/rest-client/pom.xml b/rest-client/pom.xml
index cb85c2134e..1bf93e4dc4 100644
--- a/rest-client/pom.xml
+++ b/rest-client/pom.xml
@@ -47,6 +47,10 @@
org.thingsboard.common
util
+
+ com.auth0
+ java-jwt
+
diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
index c2c0a6d794..8dcdb86eab 100644
--- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
+++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.rest.client;
+import com.auth0.jwt.JWT;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -28,9 +29,6 @@ import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
-import org.springframework.http.client.ClientHttpRequestExecution;
-import org.springframework.http.client.ClientHttpRequestInterceptor;
-import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -161,7 +159,6 @@ import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import java.io.Closeable;
-import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
@@ -171,6 +168,7 @@ import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.StringUtils.isEmpty;
@@ -178,16 +176,24 @@ import static org.thingsboard.server.common.data.StringUtils.isEmpty;
/**
* @author Andrew Shvayka
*/
-public class RestClient implements ClientHttpRequestInterceptor, Closeable {
+public class RestClient implements Closeable {
private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
+ private static final long AVG_REQUEST_TIMEOUT = TimeUnit.SECONDS.toMillis(30);
+ protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken=";
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private final ExecutorService service = ThingsBoardExecutors.newWorkStealingPool(10, getClass());
protected final RestTemplate restTemplate;
+ protected final RestTemplate loginRestTemplate;
protected final String baseURL;
- private String token;
- private String refreshToken;
- private final ObjectMapper objectMapper = new ObjectMapper();
- private ExecutorService service = ThingsBoardExecutors.newWorkStealingPool(10, getClass());
- protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken=";
+ private String username;
+ private String password;
+ private String mainToken;
+ private String refreshToken;
+ private long mainTokenExpTs;
+ private long refreshTokenExpTs;
+ private long clientServerTimeDiff;
public RestClient(String baseURL) {
this(new RestTemplate(), baseURL);
@@ -195,23 +201,25 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
public RestClient(RestTemplate restTemplate, String baseURL) {
this.restTemplate = restTemplate;
+ this.loginRestTemplate = new RestTemplate(restTemplate.getRequestFactory());
this.baseURL = baseURL;
- }
-
- @Override
- public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
- HttpRequest wrapper = new HttpRequestWrapper(request);
- wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
- ClientHttpResponse response = execution.execute(wrapper, bytes);
- if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
- synchronized (this) {
- restTemplate.getInterceptors().remove(this);
- refreshToken();
- wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
- return execution.execute(wrapper, bytes);
+ this.restTemplate.getInterceptors().add((request, bytes, execution) -> {
+ HttpRequest wrapper = new HttpRequestWrapper(request);
+ long calculatedTs = System.currentTimeMillis() + clientServerTimeDiff + AVG_REQUEST_TIMEOUT;
+ if (calculatedTs > mainTokenExpTs) {
+ synchronized (RestClient.this) {
+ if (calculatedTs > mainTokenExpTs) {
+ if (calculatedTs < refreshTokenExpTs) {
+ refreshToken();
+ } else {
+ doLogin();
+ }
+ }
+ }
}
- }
- return response;
+ wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + mainToken);
+ return execution.execute(wrapper, bytes);
+ });
}
public RestTemplate getRestTemplate() {
@@ -219,7 +227,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}
public String getToken() {
- return token;
+ return mainToken;
}
public String getRefreshToken() {
@@ -229,22 +237,32 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
public void refreshToken() {
Map refreshTokenRequest = new HashMap<>();
refreshTokenRequest.put("refreshToken", refreshToken);
- ResponseEntity tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/token", refreshTokenRequest, JsonNode.class);
- setTokenInfo(tokenInfo.getBody());
+ long ts = System.currentTimeMillis();
+ ResponseEntity tokenInfo = loginRestTemplate.postForEntity(baseURL + "/api/auth/token", refreshTokenRequest, JsonNode.class);
+ setTokenInfo(ts, tokenInfo.getBody());
}
public void login(String username, String password) {
+ this.username = username;
+ this.password = password;
+ doLogin();
+ }
+
+ private void doLogin() {
+ long ts = System.currentTimeMillis();
Map loginRequest = new HashMap<>();
loginRequest.put("username", username);
loginRequest.put("password", password);
- ResponseEntity tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/login", loginRequest, JsonNode.class);
- setTokenInfo(tokenInfo.getBody());
+ ResponseEntity tokenInfo = loginRestTemplate.postForEntity(baseURL + "/api/auth/login", loginRequest, JsonNode.class);
+ setTokenInfo(ts, tokenInfo.getBody());
}
- private void setTokenInfo(JsonNode tokenInfo) {
- this.token = tokenInfo.get("token").asText();
+ private synchronized void setTokenInfo(long ts, JsonNode tokenInfo) {
+ this.mainToken = tokenInfo.get("token").asText();
this.refreshToken = tokenInfo.get("refreshToken").asText();
- restTemplate.getInterceptors().add(this);
+ this.mainTokenExpTs = JWT.decode(this.mainToken).getExpiresAtAsInstant().toEpochMilli();
+ this.refreshTokenExpTs = JWT.decode(refreshToken).getExpiresAtAsInstant().toEpochMilli();
+ this.clientServerTimeDiff = JWT.decode(this.mainToken).getIssuedAtAsInstant().toEpochMilli() - ts;
}
public Optional getAdminSettings(String key) {
@@ -3553,9 +3571,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
@Override
public void close() {
- if (service != null) {
- service.shutdown();
- }
+ service.shutdown();
}
}