You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bigtop.apache.org by iw...@apache.org on 2026/10/20 09:49:57 UTC

(bigtop-manager) 01/01: mcp requires the same valid JWT token header as the rest of the API.

This is an automated email from the ASF dual-hosted git repository.

iwasakims pushed a commit to branch mcp-auth
in repository https://gitbox.apache.org/repos/asf/bigtop-manager.git

commit cab4b4b218d61f68871a700032329239b65bbf19
Author: Masatake Iwasaki <iw...@apache.org>
AuthorDate: Wed Oct 14 12:14:38 2026 +0900

    mcp requires the same valid JWT token header as the rest of the API.
---
 .../server/interceptor/AuthInterceptor.java        |  58 +------
 .../manager/server/interceptor/MCPInterceptor.java |  10 +-
 ...uthInterceptor.java => TokenAuthenticator.java} |  75 +++------
 .../server/interceptor/MCPInterceptorTest.java     | 172 +++++++++++++++++++++
 4 files changed, 206 insertions(+), 109 deletions(-)

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 68737c53..ccd2f3ba 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
@@ -18,38 +18,23 @@
  */
 package org.apache.bigtop.manager.server.interceptor;
 
-import org.apache.bigtop.manager.common.constants.Caches;
 import org.apache.bigtop.manager.common.utils.JsonUtils;
-import org.apache.bigtop.manager.server.enums.ApiExceptionEnum;
 import org.apache.bigtop.manager.server.holder.SessionUserHolder;
-import org.apache.bigtop.manager.server.model.vo.UserVO;
-import org.apache.bigtop.manager.server.service.UserService;
-import org.apache.bigtop.manager.server.utils.CacheUtils;
-import org.apache.bigtop.manager.server.utils.JWTUtils;
 import org.apache.bigtop.manager.server.utils.ResponseEntity;
 
-import org.apache.commons.lang3.StringUtils;
-
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.springframework.web.servlet.HandlerInterceptor;
 import org.springframework.web.servlet.ModelAndView;
 
-import com.auth0.jwt.interfaces.DecodedJWT;
-
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
-import java.util.Objects;
-import java.util.concurrent.TimeUnit;
 
 @Component
 public class AuthInterceptor implements HandlerInterceptor {
 
     @Autowired
-    private UserService userService;
-
-    @Autowired
-    private JWTUtils jwtUtils;
+    private TokenAuthenticator tokenAuthenticator;
 
     private ResponseEntity<?> responseEntity;
 
@@ -85,45 +70,8 @@ public class AuthInterceptor implements HandlerInterceptor {
     }
 
     private Boolean checkLogin(HttpServletRequest request) {
-        String token = request.getHeader("Token");
-        if (StringUtils.isBlank(token)) {
-            responseEntity = ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
-            return false;
-        }
-
-        try {
-            DecodedJWT decodedJWT = jwtUtils.resolveToken(token);
-            Long userId = decodedJWT.getClaim(JWTUtils.CLAIM_ID).asLong();
-            Integer tokenVersion =
-                    decodedJWT.getClaim(JWTUtils.CLAIM_TOKEN_VERSION).asInt();
-
-            // Check if the user exists
-            UserVO userVO = CacheUtils.getCache(Caches.CACHE_USER, userId.toString(), UserVO.class);
-            if (userVO == null) {
-                userVO = userService.get(userId);
-                if (userVO == null) {
-                    responseEntity = ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
-                    return false;
-                }
-                // Explicitly set cache expiration time
-                CacheUtils.setCache(
-                        Caches.CACHE_USER, userId.toString(), userVO, Caches.USER_EXPIRE_TIME_DAYS, TimeUnit.DAYS);
-            }
-
-            // Check if the token version matches
-            if (!Objects.equals(tokenVersion, userVO.getTokenVersion())) {
-                CacheUtils.removeCache(Caches.CACHE_USER, userId.toString());
-                responseEntity = ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
-                return false;
-            }
-
-            SessionUserHolder.setUserId(userId);
-        } catch (Exception e) {
-            responseEntity = ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
-            return false;
-        }
-
-        return true;
+        responseEntity = tokenAuthenticator.authenticate(request);
+        return responseEntity == null;
     }
 
     private Boolean checkPermission() {
diff --git a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/MCPInterceptor.java b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/MCPInterceptor.java
index f02bcab4..f0565432 100644
--- a/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/MCPInterceptor.java
+++ b/bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/MCPInterceptor.java
@@ -19,8 +19,10 @@
 package org.apache.bigtop.manager.server.interceptor;
 
 import org.apache.bigtop.manager.common.utils.JsonUtils;
+import org.apache.bigtop.manager.server.holder.SessionUserHolder;
 import org.apache.bigtop.manager.server.utils.ResponseEntity;
 
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.springframework.web.servlet.HandlerInterceptor;
 import org.springframework.web.servlet.ModelAndView;
@@ -31,6 +33,9 @@ import jakarta.servlet.http.HttpServletResponse;
 @Component
 public class MCPInterceptor implements HandlerInterceptor {
 
+    @Autowired
+    private TokenAuthenticator tokenAuthenticator;
+
     private ResponseEntity<?> responseEntity;
 
     @Override
@@ -55,11 +60,14 @@ public class MCPInterceptor implements HandlerInterceptor {
     @Override
     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
             throws Exception {
+        SessionUserHolder.clear();
+
         HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
     }
 
     private Boolean checkAuthenticated(HttpServletRequest request) {
-        return true;
+        responseEntity = tokenAuthenticator.authenticate(request);
+        return responseEntity == null;
     }
 
     private Boolean checkPermission() {
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/TokenAuthenticator.java
similarity index 58%
copy from bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/AuthInterceptor.java
copy to bigtop-manager-server/src/main/java/org/apache/bigtop/manager/server/interceptor/TokenAuthenticator.java
index 68737c53..b9aa16c2 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/TokenAuthenticator.java
@@ -19,7 +19,6 @@
 package org.apache.bigtop.manager.server.interceptor;
 
 import org.apache.bigtop.manager.common.constants.Caches;
-import org.apache.bigtop.manager.common.utils.JsonUtils;
 import org.apache.bigtop.manager.server.enums.ApiExceptionEnum;
 import org.apache.bigtop.manager.server.holder.SessionUserHolder;
 import org.apache.bigtop.manager.server.model.vo.UserVO;
@@ -32,18 +31,23 @@ import org.apache.commons.lang3.StringUtils;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
-import org.springframework.web.servlet.HandlerInterceptor;
-import org.springframework.web.servlet.ModelAndView;
 
 import com.auth0.jwt.interfaces.DecodedJWT;
 
 import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 
+/**
+ * Shared JWT-based login check used by every {@link org.springframework.web.servlet.HandlerInterceptor}
+ * that guards an HTTP entry point (REST API, MCP endpoints, ...).
+ * <p>
+ * Centralizing this logic ensures that any new entry point starts from the same
+ * "must present a valid, current token" baseline instead of a hand-rolled (and
+ * potentially forgotten) check.
+ */
 @Component
-public class AuthInterceptor implements HandlerInterceptor {
+public class TokenAuthenticator {
 
     @Autowired
     private UserService userService;
@@ -51,44 +55,17 @@ public class AuthInterceptor implements HandlerInterceptor {
     @Autowired
     private JWTUtils jwtUtils;
 
-    private ResponseEntity<?> responseEntity;
-
-    @Override
-    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
-            throws Exception {
-        if (checkLogin(request) && checkPermission()) {
-            return HandlerInterceptor.super.preHandle(request, response, handler);
-        } else {
-            response.setHeader("Content-Type", "application/json; charset=UTF-8");
-            response.getWriter().write(JsonUtils.writeAsString(responseEntity));
-            return false;
-        }
-    }
-
-    @Override
-    public void postHandle(
-            HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
-            throws Exception {
-        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
-    }
-
-    @Override
-    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
-            throws Exception {
-        clearStatus();
-
-        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
-    }
-
-    private void clearStatus() {
-        SessionUserHolder.clear();
-    }
-
-    private Boolean checkLogin(HttpServletRequest request) {
+    /**
+     * Validates the {@code Token} header on {@code request}.
+     * <p>
+     * On success, the authenticated user id is stored in {@link SessionUserHolder} and
+     * {@code null} is returned. On failure, the response body to write back is returned;
+     * the caller is expected to reject the request in that case.
+     */
+    public ResponseEntity<?> authenticate(HttpServletRequest request) {
         String token = request.getHeader("Token");
         if (StringUtils.isBlank(token)) {
-            responseEntity = ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
-            return false;
+            return ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
         }
 
         try {
@@ -102,8 +79,7 @@ public class AuthInterceptor implements HandlerInterceptor {
             if (userVO == null) {
                 userVO = userService.get(userId);
                 if (userVO == null) {
-                    responseEntity = ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
-                    return false;
+                    return ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
                 }
                 // Explicitly set cache expiration time
                 CacheUtils.setCache(
@@ -113,20 +89,13 @@ public class AuthInterceptor implements HandlerInterceptor {
             // Check if the token version matches
             if (!Objects.equals(tokenVersion, userVO.getTokenVersion())) {
                 CacheUtils.removeCache(Caches.CACHE_USER, userId.toString());
-                responseEntity = ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
-                return false;
+                return ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
             }
 
             SessionUserHolder.setUserId(userId);
+            return null;
         } catch (Exception e) {
-            responseEntity = ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
-            return false;
+            return ResponseEntity.error(ApiExceptionEnum.NEED_LOGIN);
         }
-
-        return true;
-    }
-
-    private Boolean checkPermission() {
-        return true;
     }
 }
diff --git a/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/interceptor/MCPInterceptorTest.java b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/interceptor/MCPInterceptorTest.java
new file mode 100644
index 00000000..591b11c9
--- /dev/null
+++ b/bigtop-manager-server/src/test/java/org/apache/bigtop/manager/server/interceptor/MCPInterceptorTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.interceptor;
+
+import org.apache.bigtop.manager.common.constants.Caches;
+import org.apache.bigtop.manager.common.utils.JsonUtils;
+import org.apache.bigtop.manager.server.config.JwtProperties;
+import org.apache.bigtop.manager.server.enums.ApiExceptionEnum;
+import org.apache.bigtop.manager.server.enums.LocaleKeys;
+import org.apache.bigtop.manager.server.holder.SessionUserHolder;
+import org.apache.bigtop.manager.server.model.vo.UserVO;
+import org.apache.bigtop.manager.server.service.UserService;
+import org.apache.bigtop.manager.server.utils.CacheUtils;
+import org.apache.bigtop.manager.server.utils.JWTUtils;
+import org.apache.bigtop.manager.server.utils.MessageSourceUtils;
+import org.apache.bigtop.manager.server.utils.ResponseEntity;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+/**
+ * Regression test for the MCP endpoints' authentication.
+ * <p>
+ * {@link MCPInterceptor#checkAuthenticated} must enforce the same JWT check as
+ * the regular REST API.
+ */
+@ExtendWith(MockitoExtension.class)
+class MCPInterceptorTest {
+
+    @Mock
+    private UserService userService;
+
+    private JWTUtils jwtUtils;
+
+    private MCPInterceptor mcpInterceptor;
+
+    private MockedStatic<MessageSourceUtils> messageSourceUtilsMock;
+
+    @BeforeEach
+    void setUp() {
+        // ApiExceptionEnum#getMessage() delegates to MessageSourceUtils, which requires a
+        // Spring MessageSource that isn't available in this plain unit test.
+        messageSourceUtilsMock = mockStatic(MessageSourceUtils.class);
+        messageSourceUtilsMock
+                .when(() -> MessageSourceUtils.getMessage(any(LocaleKeys.class)))
+                .thenReturn("Login required");
+
+        JwtProperties jwtProperties = new JwtProperties();
+        jwtProperties.setSecret("unit-test-secret");
+        jwtProperties.setIssuer("bigtop-manager");
+        jwtProperties.setAudience("bigtop-manager");
+        jwtProperties.setExpirationDays(7);
+        jwtUtils = new JWTUtils(jwtProperties);
+
+        TokenAuthenticator tokenAuthenticator = new TokenAuthenticator();
+        ReflectionTestUtils.setField(tokenAuthenticator, "userService", userService);
+        ReflectionTestUtils.setField(tokenAuthenticator, "jwtUtils", jwtUtils);
+
+        mcpInterceptor = new MCPInterceptor();
+        ReflectionTestUtils.setField(mcpInterceptor, "tokenAuthenticator", tokenAuthenticator);
+    }
+
+    @AfterEach
+    void tearDown() {
+        messageSourceUtilsMock.close();
+        SessionUserHolder.clear();
+        CacheUtils.clearCache(Caches.CACHE_USER);
+    }
+
+    @Test
+    void preHandle_NoTokenHeader_BlocksRequest() throws Exception {
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        boolean allowed = mcpInterceptor.preHandle(request, response, new Object());
+
+        assertFalse(allowed, "MCP endpoints must reject requests without a Token header");
+        assertNull(SessionUserHolder.getUserId());
+
+        ResponseEntity<?> body = JsonUtils.readFromString(response.getContentAsString(), ResponseEntity.class);
+        assertEquals(ApiExceptionEnum.NEED_LOGIN.getCode(), body.getCode());
+    }
+
+    @Test
+    void preHandle_InvalidToken_BlocksRequest() throws Exception {
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.addHeader("Token", "not-a-valid-jwt");
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        boolean allowed = mcpInterceptor.preHandle(request, response, new Object());
+
+        assertFalse(allowed);
+        assertNull(SessionUserHolder.getUserId());
+    }
+
+    @Test
+    void preHandle_UnknownUser_BlocksRequest() throws Exception {
+        Long userId = 99L;
+        when(userService.get(userId)).thenReturn(null);
+        String token = jwtUtils.generateToken(userId, "ghost", 1);
+
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.addHeader("Token", token);
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        boolean allowed = mcpInterceptor.preHandle(request, response, new Object());
+
+        assertFalse(allowed);
+        assertNull(SessionUserHolder.getUserId());
+    }
+
+    @Test
+    void preHandle_ValidToken_AllowsRequestAndSetsSessionUser() throws Exception {
+        Long userId = 7L;
+        UserVO userVO = new UserVO();
+        userVO.setId(userId);
+        userVO.setTokenVersion(1);
+        when(userService.get(userId)).thenReturn(userVO);
+        String token = jwtUtils.generateToken(userId, "alice", 1);
+
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.addHeader("Token", token);
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        boolean allowed = mcpInterceptor.preHandle(request, response, new Object());
+
+        assertTrue(allowed);
+        assertEquals(userId, SessionUserHolder.getUserId());
+    }
+
+    @Test
+    void afterCompletion_ClearsSessionUserHolder() throws Exception {
+        SessionUserHolder.setUserId(123L);
+
+        mcpInterceptor.afterCompletion(
+                new MockHttpServletRequest(), new MockHttpServletResponse(), new Object(), null);
+
+        assertNull(SessionUserHolder.getUserId());
+    }
+}