Copilot commented on code in PR #293:
URL: https://github.com/apache/bigtop-manager/pull/293#discussion_r3005903088


##########
dev-support/docker/containers/build.sh:
##########
@@ -112,6 +129,11 @@ create_container() {
         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"

Review Comment:
   The `grep`/`sed` patterns use `\s` to match whitespace, but POSIX 
`grep`/`sed` don’t treat `\s` as whitespace. As a result, these commands won’t 
match `secret:` lines and the “inject JWT secret” step is effectively a no-op. 
Use a portable whitespace class (e.g., `[[:space:]]`) and make the logic 
actually update/ensure the intended YAML key.
   ```suggestion
         docker exec ${container} bash -c "if grep -q '^[[:space:]]*secret:' 
/opt/bigtop-manager-server/conf/application.yml; then sed -i 
's/^\([[:space:]]*secret:\).*/\1 \${BIGTOP_MANAGER_JWT_SECRET}/' 
/opt/bigtop-manager-server/conf/application.yml; else printf '\nsecret: %s\n' 
\"\${BIGTOP_MANAGER_JWT_SECRET}\" >> 
/opt/bigtop-manager-server/conf/application.yml; fi || true"
   ```



##########
bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/JWTUtils.java:
##########
@@ -33,23 +41,69 @@ public class JWTUtils {
 
     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";
+
+    private final JwtProperties jwtProperties;
 
-    // Token validity period (days)
-    private static final int TOKEN_EXPIRATION_DAYS = 7;
+    public JWTUtils(JwtProperties jwtProperties) {
+        this.jwtProperties = jwtProperties;
+    }
 
-    public static String generateToken(Long userId, String username, Integer 
tokenVersion) {
-        Instant expireTime = Instant.now().plus(TOKEN_EXPIRATION_DAYS, 
ChronoUnit.DAYS);
+    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 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");
+        }

Review Comment:
   `resolveToken()` now requires issuer/audience and enforces presence of 
`iat`. Tokens minted by the previous implementation (no issuer/audience/iat) 
will start failing verification immediately after upgrade, forcing all clients 
to re-authenticate. If that’s intended, it should be explicitly documented as a 
breaking change; otherwise consider a compatibility mode/migration window 
(e.g., optionally accept missing fields for a period).



##########
bigtop-manager-server/src/main/resources/application.yml:
##########
@@ -67,4 +67,15 @@ springdoc:
 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

Review Comment:
   With `secret: ${BIGTOP_MANAGER_JWT_SECRET:}` and `allow-default-secret: 
false`, a default install where the env var isn’t set will allow the app to 
start but will later fail token generation/verification at runtime (typically 
as 500s). If the intention is to require an explicit secret everywhere, 
consider failing fast during startup or providing a clear dev/default profile 
so contributors don’t hit opaque runtime errors.



##########
dev-support/docker/containers/build.sh:
##########
@@ -31,6 +31,18 @@ log() {
     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"

Review Comment:
   `init_jwt_secret` says it "Generate[s] a random JWT secret", but it actually 
sets a hard-coded, well-known value. This is misleading and also makes local 
environments share the same signing key, which weakens token integrity even in 
dev. Either generate a per-run/per-dev random secret (and optionally persist 
it) or update the comment/behavior to clearly indicate it’s a fixed dev default.
   ```suggestion
   # Set a fixed default JWT secret for local dev if user didn't provide one.
   init_jwt_secret() {
     if [ -z "${BIGTOP_MANAGER_JWT_SECRET}" ]; then
       # Fixed 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 "Using fixed default BIGTOP_MANAGER_JWT_SECRET for dev-support"
   ```



##########
bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/utils/JWTUtils.java:
##########
@@ -33,23 +41,69 @@ public class JWTUtils {
 
     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";
+
+    private final JwtProperties jwtProperties;
 
-    // Token validity period (days)
-    private static final int TOKEN_EXPIRATION_DAYS = 7;
+    public JWTUtils(JwtProperties jwtProperties) {
+        this.jwtProperties = jwtProperties;
+    }
 
-    public static String generateToken(Long userId, String username, Integer 
tokenVersion) {
-        Instant expireTime = Instant.now().plus(TOKEN_EXPIRATION_DAYS, 
ChronoUnit.DAYS);
+    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 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;
     }
 
-    public static DecodedJWT resolveToken(String token) {
-        return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
+    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).");
     }

Review Comment:
   `getSigningSecret()` throws `IllegalStateException` when the secret is 
missing, which will be handled by the global exception handler as a generic 
500. That makes a common misconfiguration hard to diagnose and can surface as 
an opaque runtime failure on login/auth. Consider failing fast at startup 
(validation) or throwing/handling a domain-specific exception that returns a 
clearer configuration error response.
   ```suggestion
           throw new JwtConfigurationException(
                   "JWT secret is not configured. Please set 
bigtop-manager.security.jwt.secret (or enable allowDefaultSecret for dev 
only).");
       }
       /**
        * Exception indicating that the JWT subsystem is misconfigured, for 
example
        * when the signing secret is missing in a non-development environment.
        */
       static class JwtConfigurationException extends RuntimeException {
   
           JwtConfigurationException(String message) {
               super(message);
           }
       }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to