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());
+ }
+}