feat: implement JWT utility class with configurable properties and token management (#293)
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/BigtopManagerServer.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/BigtopManagerServer.java
index a69e7fa..fa150ea 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/BigtopManagerServer.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/BigtopManagerServer.java
@@ -18,13 +18,17 @@
*/
package org.apache.bigtop.manager.server;
+import org.apache.bigtop.manager.server.config.JwtProperties;
+
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableAsync
@EnableScheduling
+@EnableConfigurationProperties(JwtProperties.class)
@SpringBootApplication(
scanBasePackages = {
"org.apache.bigtop.manager.server",
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/config/JwtProperties.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/config/JwtProperties.java
new file mode 100644
index 0000000..797d466
--- /dev/null
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/config/JwtProperties.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * https://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.apache.bigtop.manager.server.config;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import lombok.Data;
+
+@Data
+@ConfigurationProperties(prefix = "bigtop-manager.security.jwt")
+public class JwtProperties {
+
+ /**
+ * JWT signing secret.
+ * <p>
+ * Must be set in production (e.g. via env var) to prevent forged tokens.
+ */
+ private String secret;
+
+ /** Issuer to embed and to require during verification. */
+ private String issuer = "bigtop-manager";
+
+ /** Audience to embed and to require during verification. */
+ private String audience = "bigtop-manager";
+
+ /** Token validity period in days. */
+ private int expirationDays = 7;
+
+ /**
+ * Whether to allow a built-in dev secret as fallback.
+ * <p>
+ * Keep this false in production.
+ */
+ private boolean allowDefaultSecret = false;
+}
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/AuthInterceptor.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/AuthInterceptor.java
index a7cc2e8..68737c5 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/AuthInterceptor.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/AuthInterceptor.java
@@ -48,6 +48,9 @@
@Autowired
private UserService userService;
+ @Autowired
+ private JWTUtils jwtUtils;
+
private ResponseEntity<?> responseEntity;
@Override
@@ -89,7 +92,7 @@
}
try {
- DecodedJWT decodedJWT = JWTUtils.resolveToken(token);
+ DecodedJWT decodedJWT = jwtUtils.resolveToken(token);
Long userId = decodedJWT.getClaim(JWTUtils.CLAIM_ID).asLong();
Integer tokenVersion =
decodedJWT.getClaim(JWTUtils.CLAIM_TOKEN_VERSION).asInt();
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LoginServiceImpl.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LoginServiceImpl.java
index 88d4b55..9a1c2cd 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LoginServiceImpl.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/service/impl/LoginServiceImpl.java
@@ -43,6 +43,9 @@
@Resource
private UserDao userDao;
+ @Resource
+ private JWTUtils jwtUtils;
+
@Override
public LoginVO login(LoginDTO loginDTO) {
String username = loginDTO.getUsername();
@@ -75,7 +78,7 @@
CacheUtils.setCache(
Caches.CACHE_USER, user.getId().toString(), userVO, Caches.USER_EXPIRE_TIME_DAYS, TimeUnit.DAYS);
- String token = JWTUtils.generateToken(user.getId(), user.getUsername(), user.getTokenVersion());
+ String token = jwtUtils.generateToken(user.getId(), user.getUsername(), user.getTokenVersion());
LoginVO loginVO = new LoginVO();
loginVO.setToken(token);
return loginVO;
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/JWTUtils.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/JWTUtils.java
index a0f7e26..eb36b57 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/JWTUtils.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/JWTUtils.java
@@ -18,13 +18,21 @@
*/
package org.apache.bigtop.manager.server.utils;
+import org.apache.bigtop.manager.server.config.JwtProperties;
+
+import org.springframework.stereotype.Component;
+
import com.auth0.jwt.JWT;
+import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
+import java.util.Date;
+@Component
public class JWTUtils {
public static final String CLAIM_ID = "id";
@@ -33,23 +41,69 @@
public static final String CLAIM_TOKEN_VERSION = "token_version";
- protected static final String SIGN = "r0PGVyvjKOxUBwGt";
+ /**
+ * Dev-only fallback secret to preserve local boot for contributors.
+ * <p>
+ * In production, configure `bigtop-manager.security.jwt.secret`.
+ */
+ static final String DEFAULT_DEV_SECRET = "r0PGVyvjKOxUBwGt";
- // Token validity period (days)
- private static final int TOKEN_EXPIRATION_DAYS = 7;
+ private final JwtProperties jwtProperties;
- public static String generateToken(Long userId, String username, Integer tokenVersion) {
- Instant expireTime = Instant.now().plus(TOKEN_EXPIRATION_DAYS, ChronoUnit.DAYS);
+ public JWTUtils(JwtProperties jwtProperties) {
+ this.jwtProperties = jwtProperties;
+ }
+
+ public String generateToken(Long userId, String username, Integer tokenVersion) {
+ Instant now = Instant.now();
+ Instant expireTime = now.plus(jwtProperties.getExpirationDays(), ChronoUnit.DAYS);
return JWT.create()
+ .withIssuer(jwtProperties.getIssuer())
+ .withAudience(jwtProperties.getAudience())
+ .withIssuedAt(Date.from(now))
.withClaim(CLAIM_ID, userId)
.withClaim(CLAIM_USERNAME, username)
.withClaim(CLAIM_TOKEN_VERSION, tokenVersion)
- .withExpiresAt(expireTime)
- .sign(Algorithm.HMAC256(SIGN));
+ .withExpiresAt(Date.from(expireTime))
+ .sign(Algorithm.HMAC256(getSigningSecret()));
}
- public static DecodedJWT resolveToken(String token) {
- return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
+ public DecodedJWT resolveToken(String token) throws JWTVerificationException {
+ Algorithm algorithm = Algorithm.HMAC256(getSigningSecret());
+ JWTVerifier verifier = JWT.require(algorithm)
+ .withIssuer(jwtProperties.getIssuer())
+ .withAudience(jwtProperties.getAudience())
+ .build();
+
+ DecodedJWT decodedJWT = verifier.verify(token);
+
+ // Enforce issued-at to mitigate tokens without freshness metadata.
+ Date issuedAt = decodedJWT.getIssuedAt();
+ if (issuedAt == null) {
+ throw new JWTVerificationException("Missing iat");
+ }
+
+ // Reject tokens issued too far in the future (clock skew).
+ Instant now = Instant.now();
+ if (issuedAt.toInstant().isAfter(now.plus(5, ChronoUnit.MINUTES))) {
+ throw new JWTVerificationException("iat is in the future");
+ }
+
+ return decodedJWT;
+ }
+
+ private String getSigningSecret() {
+ String secret = jwtProperties.getSecret();
+ if (secret != null && !secret.isBlank()) {
+ return secret;
+ }
+
+ if (jwtProperties.isAllowDefaultSecret()) {
+ return DEFAULT_DEV_SECRET;
+ }
+
+ throw new IllegalStateException(
+ "JWT secret is not configured. Please set bigtop-manager.security.jwt.secret (or enable allowDefaultSecret for dev only).");
}
}
diff --git a/bigtop-manager-server/src/main/resources/application.yml b/bigtop-manager-server/src/main/resources/application.yml
index 860573b..2b8886e 100644
--- a/bigtop-manager-server/src/main/resources/application.yml
+++ b/bigtop-manager-server/src/main/resources/application.yml
@@ -67,4 +67,15 @@
pagehelper:
reasonable: false
params: count=countSql
- support-methods-arguments: true
\ No newline at end of file
+ support-methods-arguments: true
+
+bigtop-manager:
+ security:
+ jwt:
+ # IMPORTANT: Set a strong, random secret in production (e.g. via env var BIGTOP_MANAGER_JWT_SECRET)
+ secret: ${BIGTOP_MANAGER_JWT_SECRET:}
+ issuer: bigtop-manager
+ audience: bigtop-manager
+ expiration-days: 7
+ # Dev-only compatibility switch. Keep false in production.
+ allow-default-secret: false
diff --git a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/LoginServiceTest.java b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/LoginServiceTest.java
index 86c06db..68bcd90 100644
--- a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/LoginServiceTest.java
+++ b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/service/LoginServiceTest.java
@@ -27,6 +27,7 @@
import org.apache.bigtop.manager.server.model.dto.LoginDTO;
import org.apache.bigtop.manager.server.service.impl.LoginServiceImpl;
import org.apache.bigtop.manager.server.utils.CacheUtils;
+import org.apache.bigtop.manager.server.utils.JWTUtils;
import org.apache.bigtop.manager.server.utils.PasswordUtils;
import org.apache.bigtop.manager.server.utils.Pbkdf2Utils;
@@ -51,6 +52,9 @@
@Mock
private UserDao userDao;
+ @Mock
+ private JWTUtils jwtUtils;
+
@InjectMocks
private LoginService loginService = new LoginServiceImpl();
@@ -107,6 +111,7 @@
LoginDTO loginDTO = createLoginDTO(RAW_PASSWORD);
when(userDao.findByUsername(any())).thenReturn(mockUser);
+ when(jwtUtils.generateToken(any(), any(), any())).thenReturn("test-token");
Object result = loginService.login(loginDTO);
assertNotNull(result);
diff --git a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/utils/JWTUtilsTest.java b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/utils/JWTUtilsTest.java
index e931609..ed2c68a 100644
--- a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/utils/JWTUtilsTest.java
+++ b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/utils/JWTUtilsTest.java
@@ -18,6 +18,8 @@
*/
package org.apache.bigtop.manager.server.utils;
+import org.apache.bigtop.manager.server.config.JwtProperties;
+
import org.junit.jupiter.api.Test;
import com.auth0.jwt.JWT;
@@ -35,15 +37,26 @@
public class JWTUtilsTest {
+ private static JWTUtils newJwtUtilsWithDevSecretAllowed() {
+ JwtProperties props = new JwtProperties();
+ props.setAllowDefaultSecret(true);
+ props.setIssuer("bigtop-manager");
+ props.setAudience("bigtop-manager");
+ props.setExpirationDays(7);
+ return new JWTUtils(props);
+ }
+
@Test
public void testGenerateTokenNormal() {
+ JWTUtils jwtUtils = newJwtUtilsWithDevSecretAllowed();
+
Long id = 1L;
String username = "testUser";
Integer tokenVersion = 1;
- String token = JWTUtils.generateToken(id, username, tokenVersion);
+ String token = jwtUtils.generateToken(id, username, tokenVersion);
assertNotNull(token);
- DecodedJWT decodedJWT = JWTUtils.resolveToken(token);
+ DecodedJWT decodedJWT = jwtUtils.resolveToken(token);
assertEquals(id, decodedJWT.getClaim(JWTUtils.CLAIM_ID).asLong());
assertEquals(username, decodedJWT.getClaim(JWTUtils.CLAIM_USERNAME).asString());
assertEquals(
@@ -52,6 +65,8 @@
@Test
public void testResolveTokenExpired() {
+ JWTUtils jwtUtils = newJwtUtilsWithDevSecretAllowed();
+
Long id = 2L;
String username = "expiredUser";
Calendar calendar = Calendar.getInstance();
@@ -59,33 +74,67 @@
Date date = calendar.getTime();
String token = JWT.create()
+ .withIssuer("bigtop-manager")
+ .withAudience("bigtop-manager")
+ .withIssuedAt(new Date())
.withClaim(JWTUtils.CLAIM_ID, id)
.withClaim(JWTUtils.CLAIM_USERNAME, username)
.withExpiresAt(date)
- .sign(Algorithm.HMAC256(JWTUtils.SIGN));
+ .sign(Algorithm.HMAC256(JWTUtils.DEFAULT_DEV_SECRET));
- assertThrows(JWTVerificationException.class, () -> JWTUtils.resolveToken(token));
+ assertThrows(JWTVerificationException.class, () -> jwtUtils.resolveToken(token));
}
@Test
public void testResolveTokenIllegal() {
+ JWTUtils jwtUtils = newJwtUtilsWithDevSecretAllowed();
+
String illegalToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
- assertThrows(JWTVerificationException.class, () -> JWTUtils.resolveToken(illegalToken));
+ assertThrows(JWTVerificationException.class, () -> jwtUtils.resolveToken(illegalToken));
}
@Test
public void testResolveTokenWrongFormat() {
+ JWTUtils jwtUtils = newJwtUtilsWithDevSecretAllowed();
+
String wrongFormatToken = "wrong_format_token";
- assertThrows(JWTDecodeException.class, () -> JWTUtils.resolveToken(wrongFormatToken));
+ assertThrows(JWTDecodeException.class, () -> jwtUtils.resolveToken(wrongFormatToken));
}
@Test
public void testGenerateTokenUsernameEmpty() {
- String token = JWTUtils.generateToken(1L, "", 1);
+ JWTUtils jwtUtils = newJwtUtilsWithDevSecretAllowed();
+
+ String token = jwtUtils.generateToken(1L, "", 1);
assertNotNull(token);
- DecodedJWT decodedJWT = JWTUtils.resolveToken(token);
+ DecodedJWT decodedJWT = jwtUtils.resolveToken(token);
assertEquals("", decodedJWT.getClaim(JWTUtils.CLAIM_USERNAME).asString());
}
+
+ @Test
+ public void testResolveTokenMissingIatRejected() {
+ JWTUtils jwtUtils = newJwtUtilsWithDevSecretAllowed();
+
+ String token = JWT.create()
+ .withIssuer("bigtop-manager")
+ .withAudience("bigtop-manager")
+ .withClaim(JWTUtils.CLAIM_ID, 1L)
+ .withClaim(JWTUtils.CLAIM_TOKEN_VERSION, 1)
+ .withExpiresAt(new Date(System.currentTimeMillis() + 60_000))
+ // intentionally no iat
+ .sign(Algorithm.HMAC256(JWTUtils.DEFAULT_DEV_SECRET));
+
+ assertThrows(JWTVerificationException.class, () -> jwtUtils.resolveToken(token));
+ }
+
+ @Test
+ public void testSecretRequiredByDefault() {
+ JwtProperties props = new JwtProperties();
+ props.setAllowDefaultSecret(false);
+ JWTUtils jwtUtils = new JWTUtils(props);
+
+ assertThrows(IllegalStateException.class, () -> jwtUtils.generateToken(1L, "u", 1));
+ }
}
diff --git a/dev-support/docker/containers/build.sh b/dev-support/docker/containers/build.sh
index 88131b3..37549f8 100755
--- a/dev-support/docker/containers/build.sh
+++ b/dev-support/docker/containers/build.sh
@@ -31,6 +31,18 @@
echo -e "\033[32m[LOG] $1\033[0m"
}
+# Generate a random JWT secret for local dev if user didn't provide one.
+init_jwt_secret() {
+ if [ -z "${BIGTOP_MANAGER_JWT_SECRET}" ]; then
+ # Dev-support default. Override by exporting BIGTOP_MANAGER_JWT_SECRET before running this script.
+ BIGTOP_MANAGER_JWT_SECRET="r0PGVyvjKOxUBwGt"
+ export BIGTOP_MANAGER_JWT_SECRET
+ log "Defaulted BIGTOP_MANAGER_JWT_SECRET for dev-support"
+ else
+ log "Using provided BIGTOP_MANAGER_JWT_SECRET for dev-support"
+ fi
+}
+
build() {
log "Build on docker: $SKIP_COMPILE"
if ! $SKIP_COMPILE; then
@@ -54,6 +66,7 @@
create() {
log "Create Containers!!!"
+ init_jwt_secret
docker network inspect bigtop-manager >/dev/null 2>&1 || docker network create --driver bridge bigtop-manager
create_db
create_container
@@ -65,13 +78,17 @@
container_name="bm-$i"
log "Create ${container_name}"
if [ $i -eq 1 ]; then
- docker run -itd -p 15005:5005 -p 15006:5006 -p 18080:8080 --name ${container_name} --hostname ${container_name} --network bigtop-manager --cap-add=SYS_TIME bigtop-manager/develop:${OS}
+ docker run -itd -p 15005:5005 -p 15006:5006 -p 18080:8080 \
+ -e BIGTOP_MANAGER_JWT_SECRET="${BIGTOP_MANAGER_JWT_SECRET}" \
+ --name ${container_name} --hostname ${container_name} --network bigtop-manager --cap-add=SYS_TIME bigtop-manager/develop:${OS}
docker cp ../../../bigtop-manager-dist/target/apache-bigtop-manager-*-server.tar.gz ${container_name}:/opt/bigtop-manager-server.tar.gz
docker exec ${container_name} bash -c "cd /opt && tar -zxvf bigtop-manager-server.tar.gz"
docker exec ${container_name} bash -c "ssh-keygen -f '/root/.ssh/id_rsa' -N '' -t rsa"
SERVER_PUB_KEY=`docker exec ${container_name} /bin/cat /root/.ssh/id_rsa.pub`
else
- docker run -itd --name ${container_name} --hostname ${container_name} --network bigtop-manager --cap-add=SYS_TIME bigtop-manager/develop:${OS}
+ docker run -itd \
+ -e BIGTOP_MANAGER_JWT_SECRET="${BIGTOP_MANAGER_JWT_SECRET}" \
+ --name ${container_name} --hostname ${container_name} --network bigtop-manager --cap-add=SYS_TIME bigtop-manager/develop:${OS}
fi
docker cp ../../../bigtop-manager-dist/target/apache-bigtop-manager-*-agent.tar.gz ${container_name}:/opt/bigtop-manager-agent.tar.gz
@@ -112,6 +129,11 @@
docker exec ${container} bash -c "PGPASSWORD=postgres psql -h bm-postgres -p5432 -U postgres -d bigtop_manager -f /opt/bigtop-manager-server/ddl/PostgreSQL-DDL-CREATE.sql"
docker exec ${container} bash -c "sed -i 's/localhost:5432/bm-postgres:5432/' /opt/bigtop-manager-server/conf/application.yml"
fi
+
+ # Inject JWT secret into server config inside container (for environments where env vars are not propagated)
+ docker exec ${container} bash -c "grep -q '^\s*secret:' /opt/bigtop-manager-server/conf/application.yml || true"
+ docker exec ${container} bash -c "sed -i 's/^\(\s*secret:\).*/\1 \${BIGTOP_MANAGER_JWT_SECRET}/' /opt/bigtop-manager-server/conf/application.yml || true"
+
docker exec ${container} bash -c "nohup /bin/bash /opt/bigtop-manager-server/bin/server.sh start --debug > /dev/null 2>&1 &"
fi
log "All Service Started!!!"