From 54f9a8a7038e60efee9058cc38f85c030870087f Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Sun, 27 Jul 2025 20:44:09 +0530 Subject: [PATCH 1/4] JWT auth added for MCP server's api calls --- deploy/docker/docker-compose.yml | 5 ++ deploy/helm/templates/chatbot/config.yaml | 5 +- deploy/helm/values.yaml | 3 + services/chatbot/src/mcpserver/config.py | 11 ++++ services/chatbot/src/mcpserver/server.py | 57 +++++++++---------- .../chatbot/src/mcpserver/tool_helpers.py | 4 +- .../java/com/crapi/constant/UserMessage.java | 3 + .../controller/ManagementControlller.java | 8 +++ .../crapi/service/Impl/UserServiceImpl.java | 53 +++++++++++++---- .../java/com/crapi/service/UserService.java | 2 + 10 files changed, 105 insertions(+), 46 deletions(-) create mode 100644 services/chatbot/src/mcpserver/config.py diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 50e2d85a..c74f368c 100755 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -156,6 +156,8 @@ services: environment: - TLS_ENABLED=${TLS_ENABLED:-false} - SERVER_PORT=${CHATBOT_SERVER_PORT:-5002} + - WEB_SERVICE=crapi-web + - IDENTITY_SERVICE=crapi-identity:${IDENTITY_SERVER_PORT:-8080} - DB_NAME=crapi - DB_USER=admin - DB_PASSWORD=crapisecretpassword @@ -166,6 +168,9 @@ services: - MONGO_DB_USER=admin - MONGO_DB_PASSWORD=crapisecretpassword - MONGO_DB_NAME=crapi + - API_USER=admin@example.com + - API_PASSWORD=Admin!123 + - API_AUTH_TYPE=jwt - DEFAULT_MODEL=gpt-4o-mini - CHROMA_PERSIST_DIRECTORY=/app/vectorstore # - CHATBOT_OPENAI_API_KEY= diff --git a/deploy/helm/templates/chatbot/config.yaml b/deploy/helm/templates/chatbot/config.yaml index 215d9af8..3dc9c570 100644 --- a/deploy/helm/templates/chatbot/config.yaml +++ b/deploy/helm/templates/chatbot/config.yaml @@ -8,7 +8,7 @@ metadata: data: SERVER_PORT: {{ .Values.chatbot.port | quote }} IDENTITY_SERVICE: {{ .Values.identity.service.name }}:{{ .Values.identity.port }} - WEB_SERVICE: {{ .Values.web.service.name }}:{{ .Values.web.port }} + WEB_SERVICE: {{ .Values.web.service.name }} TLS_ENABLED: {{ .Values.tlsEnabled | quote }} DB_HOST: {{ .Values.postgresdb.service.name }} DB_USER: {{ .Values.postgresdb.config.postgresUser }} @@ -23,3 +23,6 @@ data: CHATBOT_OPENAI_API_KEY: {{ .Values.openAIApiKey }} DEFAULT_MODEL: {{ .Values.chatbot.config.defaultModel | quote }} CHROMA_PERSIST_DIRECTORY: {{ .Values.chatbot.config.chromaPersistDirectory | quote }} + API_USER: {{ .Values.chatbot.config.apiUser | quote }} + API_PASSWORD: {{ .Values.chatbot.config.apiPassword | quote }} + API_AUTH_TYPE: {{ .Values.chatbot.config.apiAuthType | quote }} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index 030bf65c..c7bd8274 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -153,6 +153,9 @@ chatbot: secretKey: crapi defaultModel: gpt-4o-mini chromaPersistDirectory: /app/vectorstore + apiUser: admin@example.com + apiPassword: Admin!123 + apiAuthType: jwt storage: # type: "manual" # pv: diff --git a/services/chatbot/src/mcpserver/config.py b/services/chatbot/src/mcpserver/config.py new file mode 100644 index 00000000..b444d716 --- /dev/null +++ b/services/chatbot/src/mcpserver/config.py @@ -0,0 +1,11 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +class Config: + WEB_SERVICE = os.getenv("WEB_SERVICE", "crapi-web") + IDENTITY_SERVICE = os.getenv("IDENTITY_SERVICE", "crapi-identity:8080") + CHROMA_PERSIST_DIRECTORY = os.getenv("CHROMA_PERSIST_DIRECTORY", "/app/vectorstore") + TLS_ENABLED = os.getenv("TLS_ENABLED", "false").lower() in ("true", "1", "yes") + API_AUTH_TYPE = os.getenv("API_AUTH_TYPE", "jwt") # Toggle between "apikey" and "jwt" based on your auth type \ No newline at end of file diff --git a/services/chatbot/src/mcpserver/server.py b/services/chatbot/src/mcpserver/server.py index 78760626..8ea29d46 100644 --- a/services/chatbot/src/mcpserver/server.py +++ b/services/chatbot/src/mcpserver/server.py @@ -2,6 +2,7 @@ from fastmcp import FastMCP, settings import json import os +from .config import Config import logging import time from .tool_helpers import ( @@ -15,58 +16,52 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) -WEB_SERVICE = os.environ.get("WEB_SERVICE", "crapi-web") -IDENTITY_SERVICE = os.environ.get("IDENTITY_SERVICE", "crapi-identity:8080") -TLS_ENABLED = os.environ.get("TLS_ENABLED", "false").lower() in ("true", "1", "yes") -BASE_URL = f"{'https' if TLS_ENABLED else 'http'}://{WEB_SERVICE}" -BASE_IDENTITY_URL = f"{'https' if TLS_ENABLED else 'http'}://{IDENTITY_SERVICE}" +BASE_URL = f"{'https' if Config.TLS_ENABLED else 'http'}://{Config.WEB_SERVICE}" +BASE_IDENTITY_URL = f"{'https' if Config.TLS_ENABLED else 'http'}://{Config.IDENTITY_SERVICE}" +AUTH_TYPE_MAPPING = { + "apikey": "apiKey", + "jwt": "token" +} +CLIENT_AUTH = None -API_USER = os.environ.get("API_USER", "admin@example.com") -API_PASSWORD = os.environ.get("API_PASSWORD", "Admin!123") -API_URL = f"{'https' if TLS_ENABLED else 'http'}://{WEB_SERVICE}" - -API_KEY = None -API_AUTH_TYPE = "ApiKey" - -def get_api_key(): - global API_KEY - # Try 5 times to get API key +def get_client_auth(): + global CLIENT_AUTH + # Try 5 times to get client auth MAX_ATTEMPTS = 5 for i in range(MAX_ATTEMPTS): - logger.info(f"Attempt {i+1} to get API key...") - if API_KEY is None: - login_body = {"email": API_USER, "password": API_PASSWORD} - apikey_url = f"{BASE_IDENTITY_URL}/identity/management/user/apikey" + logger.info(f"Attempt {i+1} to get client auth...") + if CLIENT_AUTH is None: + login_body = {"email": Config.API_USER, "password": Config.API_PASSWORD} + auth_url = f"{BASE_IDENTITY_URL}/identity/management/user/{Config.API_AUTH_TYPE}" headers = { "Content-Type": "application/json", } with httpx.Client( - base_url=API_URL, + base_url=BASE_URL, headers=headers, ) as client: - response = client.post(apikey_url, json=login_body) + response = client.post(auth_url, json=login_body) if response.status_code != 200: if i == MAX_ATTEMPTS - 1: - logger.error(f"Failed to get API key after {i+1} attempts: {response.status_code} {response.text}") - raise Exception(f"Failed to get API key after {i+1} attempts: {response.status_code} {response.text}") - logger.error(f"Failed to get API key in attempt {i+1}: {response.status_code} {response.text}. Sleeping for {i} seconds...") + logger.error(f"Failed to get client auth after {i+1} attempts: {response.status_code} {response.text}") + raise Exception(f"Failed to get client auth after {i+1} attempts: {response.status_code} {response.text}") + logger.error(f"Failed to get client auth in attempt {i+1}: {response.status_code} {response.text}. Sleeping for {i} seconds...") time.sleep(i) response_json = response.json() logger.info(f"Response: {response_json}") - API_KEY = response_json.get("apiKey") - logger.info(f"Chatbot API Key: {API_KEY}") - return API_KEY - return API_KEY - + CLIENT_AUTH = f"{response_json.get('type')} {response_json.get(AUTH_TYPE_MAPPING[Config.API_AUTH_TYPE])}" + logger.info(f"MCP Server API Auth: {CLIENT_AUTH}") + return CLIENT_AUTH + return CLIENT_AUTH # Async HTTP client for API calls def get_http_client(): """Create and configure the HTTP client with appropriate authentication.""" headers = { - "Authorization": "ApiKey " + get_api_key(), + "Authorization": get_client_auth(), } return httpx.AsyncClient( - base_url=API_URL, + base_url=BASE_URL, headers=headers, ) diff --git a/services/chatbot/src/mcpserver/tool_helpers.py b/services/chatbot/src/mcpserver/tool_helpers.py index d78066ce..8f8a68f6 100644 --- a/services/chatbot/src/mcpserver/tool_helpers.py +++ b/services/chatbot/src/mcpserver/tool_helpers.py @@ -3,12 +3,10 @@ from langchain_community.vectorstores import Chroma from langchain.prompts import PromptTemplate from chatbot.extensions import db -from chatbot.config import Config +from .config import Config from langchain.chains import RetrievalQA from langchain_openai import ChatOpenAI -retrieval_index_path = "/app/resources/chat_index" - async def get_any_api_key(): if os.environ.get("CHATBOT_OPENAI_API_KEY"): return os.environ.get("CHATBOT_OPENAI_API_KEY") diff --git a/services/identity/src/main/java/com/crapi/constant/UserMessage.java b/services/identity/src/main/java/com/crapi/constant/UserMessage.java index f1eead17..e18292a0 100644 --- a/services/identity/src/main/java/com/crapi/constant/UserMessage.java +++ b/services/identity/src/main/java/com/crapi/constant/UserMessage.java @@ -23,6 +23,9 @@ public class UserMessage { "Api Key generated successfully. Use it in authorization header with ApiKey prefix."; public static final String API_KEY_GENERATION_FAILED = "Api Key generation failed! Only permitted for admin users."; + public static final String JWT_TOKEN_GENERATED_MESSAGE = + "JWT Token generated successfully. Use it in authorization header with Bearer prefix."; + public static final String JWT_TOKEN_GENERATION_FAILED = "JWT Token generation failed!"; public static final String ACCOUNT_LOCK_MESSAGE = "User account has been locked."; public static final String ACCOUNT_LOCKED_MESSAGE = "User account is locked. Retry login with MFA to unlock."; diff --git a/services/identity/src/main/java/com/crapi/controller/ManagementControlller.java b/services/identity/src/main/java/com/crapi/controller/ManagementControlller.java index 94697315..530c3146 100644 --- a/services/identity/src/main/java/com/crapi/controller/ManagementControlller.java +++ b/services/identity/src/main/java/com/crapi/controller/ManagementControlller.java @@ -60,4 +60,12 @@ public ResponseEntity generateApiKey( ApiKeyResponse response = userService.generateApiKey(request, loginForm); return ResponseEntity.status(HttpStatus.OK).body(response); } + + @PostMapping("/user/jwt") + public ResponseEntity generateJwt( + @RequestBody LoginForm loginForm, HttpServletRequest request) + throws UnsupportedEncodingException { + JwtResponse response = userService.generateJwtToken(request, loginForm); + return ResponseEntity.status(HttpStatus.OK).body(response); + } } diff --git a/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java b/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java index 2cc481bb..056b869c 100644 --- a/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java +++ b/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java @@ -469,19 +469,17 @@ public JwtResponse unlockAccount( @Override @Transactional public ApiKeyResponse generateApiKey(HttpServletRequest request, LoginForm loginForm) { - // if user is unauthenticated, use loginForm else user token to authenticate + Authentication authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(loginForm.getEmail(), loginForm.getPassword())); + if (authentication == null) { + return new ApiKeyResponse(null, UserMessage.INVALID_CREDENTIALS); + } + log.info("Generate Api Key for user: {}", loginForm.getEmail()); User user; if (request == null || jwtAuthTokenFilter.getToken(request) == null) { user = userRepository.findByEmail(loginForm.getEmail()); } else { - log.info("Generate Api Key for user: {}", loginForm.getEmail()); - Authentication authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken( - loginForm.getEmail(), loginForm.getPassword())); - if (authentication == null) { - return new ApiKeyResponse(null, UserMessage.INVALID_CREDENTIALS); - } user = getUserFromToken(request); } if (user == null) { @@ -493,14 +491,47 @@ public ApiKeyResponse generateApiKey(HttpServletRequest request, LoginForm login log.debug("Api Key already generated for user: {}", user.getEmail()); return new ApiKeyResponse(user.getApiKey()); } - log.info("Generate Api Key for user in token: {}", user.getEmail()); String apiKey = ApiKeyGenerator.generateRandom(512); - log.debug("Api Key for user in token {}: {}", user.getEmail(), apiKey); + log.debug("Api Key for user {}: {}", user.getEmail(), apiKey); user.setApiKey(apiKey); userRepository.save(user); return new ApiKeyResponse(user.getApiKey(), UserMessage.API_KEY_GENERATED_MESSAGE); } + /** + * @param request None + * @param loginForm LoginForm with user email and password + * @return JwtResponse with generated JWT token + */ + @Override + @Transactional + public JwtResponse generateJwtToken(HttpServletRequest request, LoginForm loginForm) { + Authentication authentication = + authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(loginForm.getEmail(), loginForm.getPassword())); + if (authentication == null) { + return new JwtResponse(null, UserMessage.INVALID_CREDENTIALS); + } + log.info("Generate JWT token for user: {}", loginForm.getEmail()); + User user; + if (request == null || jwtAuthTokenFilter.getToken(request) == null) { + user = userRepository.findByEmail(loginForm.getEmail()); + } else { + user = getUserFromToken(request); + } + if (user == null) { + log.debug("User not found to generate JWT token"); + return new JwtResponse(null, UserMessage.INVALID_CREDENTIALS); + } + String jwt = jwtProvider.generateJwtToken(user); + log.debug("JWT token for user {}: {}", user.getEmail(), jwt); + if (jwt != null) { + return new JwtResponse(jwt, UserMessage.JWT_TOKEN_GENERATED_MESSAGE); + } else { + return new JwtResponse(null, UserMessage.JWT_TOKEN_GENERATION_FAILED); + } + } + /** * @param changePhoneForm contains old phone number and new phone number, api will send otp to * change number to email address. diff --git a/services/identity/src/main/java/com/crapi/service/UserService.java b/services/identity/src/main/java/com/crapi/service/UserService.java index 959eca31..d645667b 100644 --- a/services/identity/src/main/java/com/crapi/service/UserService.java +++ b/services/identity/src/main/java/com/crapi/service/UserService.java @@ -57,4 +57,6 @@ ResponseEntity authenticateUserLogin(LoginForm loginForm) CRAPIResponse lockAccount(HttpServletRequest request, LockAccountForm lockAccountForm); ApiKeyResponse generateApiKey(HttpServletRequest request, LoginForm loginForm); + + JwtResponse generateJwtToken(HttpServletRequest request, LoginForm loginForm); } From 0ba5b2b8e03784d901685c82ced84c86c6b9adcd Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Sun, 27 Jul 2025 21:16:46 +0530 Subject: [PATCH 2/4] Updated test --- .../test/java/com/crapi/service/Impl/UserServiceImplTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java b/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java index 6af78d31..b8124dde 100644 --- a/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java +++ b/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java @@ -64,6 +64,7 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; @RunWith(MockitoJUnitRunner.class) @@ -152,6 +153,9 @@ public void testAuthenticateUserApiKey() throws UnsupportedEncodingException { LoginForm loginForm = getDummyLoginForm(); User user = getDummyUser(); user.setApiKey("sampleApiKey"); + Authentication mockAuth = Mockito.mock(Authentication.class); + Mockito.when(authenticationManager.authenticate(Mockito.any(Authentication.class))) + .thenReturn(mockAuth); Mockito.when(userRepository.findByEmail(Mockito.anyString())).thenReturn(user); ApiKeyResponse jwtResponse = userService.generateApiKey(getMockHttpRequest(), loginForm); Assertions.assertEquals(jwtResponse.getApiKey(), "sampleApiKey"); From 23a546ddb52c1a51d58bab892670e5ac34f18533 Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Tue, 29 Jul 2025 04:02:51 +0530 Subject: [PATCH 3/4] Removed random api key flow as requested and added check to allow only admins to generate API key --- deploy/docker/docker-compose.yml | 3 +- deploy/helm/templates/chatbot/config.yaml | 2 +- deploy/helm/templates/identity/config.yaml | 1 + deploy/helm/values.yaml | 3 +- services/chatbot/src/mcpserver/config.py | 6 ++- services/chatbot/src/mcpserver/server.py | 34 ++++++------ .../com/crapi/config/JwtAuthTokenFilter.java | 2 +- .../java/com/crapi/config/JwtProvider.java | 26 ++++----- .../java/com/crapi/constant/UserMessage.java | 3 -- .../controller/ManagementControlller.java | 8 --- .../crapi/service/Impl/UserServiceImpl.java | 53 ++++++------------- .../java/com/crapi/service/UserService.java | 2 - .../java/com/crapi/utils/ApiKeyGenerator.java | 42 --------------- .../src/main/resources/application.properties | 1 + .../service/Impl/UserServiceImplTest.java | 2 + 15 files changed, 58 insertions(+), 130 deletions(-) delete mode 100644 services/identity/src/main/java/com/crapi/utils/ApiKeyGenerator.java diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index c74f368c..9e334079 100755 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -48,6 +48,7 @@ services: - TLS_KEYSTORE_PASSWORD=passw0rd - TLS_KEY_PASSWORD=passw0rd - TLS_KEY_ALIAS=identity + - ADMIN_EMAILS=admin@example.com depends_on: postgresdb: condition: service_healthy @@ -170,7 +171,7 @@ services: - MONGO_DB_NAME=crapi - API_USER=admin@example.com - API_PASSWORD=Admin!123 - - API_AUTH_TYPE=jwt + - OPENAPI_SPEC=/app/resources/crapi-openapi-spec.json - DEFAULT_MODEL=gpt-4o-mini - CHROMA_PERSIST_DIRECTORY=/app/vectorstore # - CHATBOT_OPENAI_API_KEY= diff --git a/deploy/helm/templates/chatbot/config.yaml b/deploy/helm/templates/chatbot/config.yaml index 3dc9c570..06db4fc1 100644 --- a/deploy/helm/templates/chatbot/config.yaml +++ b/deploy/helm/templates/chatbot/config.yaml @@ -25,4 +25,4 @@ data: CHROMA_PERSIST_DIRECTORY: {{ .Values.chatbot.config.chromaPersistDirectory | quote }} API_USER: {{ .Values.chatbot.config.apiUser | quote }} API_PASSWORD: {{ .Values.chatbot.config.apiPassword | quote }} - API_AUTH_TYPE: {{ .Values.chatbot.config.apiAuthType | quote }} + OPENAPI_SPEC: {{ .Values.chatbot.config.openapiSpec | quote }} diff --git a/deploy/helm/templates/identity/config.yaml b/deploy/helm/templates/identity/config.yaml index 98046df2..15679897 100644 --- a/deploy/helm/templates/identity/config.yaml +++ b/deploy/helm/templates/identity/config.yaml @@ -37,3 +37,4 @@ data: TLS_KEYSTORE_PASSWORD: {{ .Values.identity.config.keyStorePassword }} TLS_KEY_PASSWORD: {{ .Values.identity.config.keyPassword }} TLS_KEY_ALIAS: {{ .Values.identity.config.keyAlias }} + ADMIN_EMAILS: {{ .Values.identity.config.adminEmails | quote }} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index c7bd8274..71373a17 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -77,6 +77,7 @@ identity: keyStorePassword: passw0rd keyPassword: passw0rd keyAlias: identity + adminEmails: admin@example.com deploymentLabels: app: crapi-identity podLabels: @@ -155,7 +156,7 @@ chatbot: chromaPersistDirectory: /app/vectorstore apiUser: admin@example.com apiPassword: Admin!123 - apiAuthType: jwt + openapiSpec: /app/resources/crapi-openapi-spec.json storage: # type: "manual" # pv: diff --git a/services/chatbot/src/mcpserver/config.py b/services/chatbot/src/mcpserver/config.py index b444d716..49ea9fe8 100644 --- a/services/chatbot/src/mcpserver/config.py +++ b/services/chatbot/src/mcpserver/config.py @@ -4,8 +4,10 @@ load_dotenv() class Config: + TLS_ENABLED = os.getenv("TLS_ENABLED", "false").lower() in ("true", "1", "yes") WEB_SERVICE = os.getenv("WEB_SERVICE", "crapi-web") IDENTITY_SERVICE = os.getenv("IDENTITY_SERVICE", "crapi-identity:8080") CHROMA_PERSIST_DIRECTORY = os.getenv("CHROMA_PERSIST_DIRECTORY", "/app/vectorstore") - TLS_ENABLED = os.getenv("TLS_ENABLED", "false").lower() in ("true", "1", "yes") - API_AUTH_TYPE = os.getenv("API_AUTH_TYPE", "jwt") # Toggle between "apikey" and "jwt" based on your auth type \ No newline at end of file + OPENAPI_SPEC = os.getenv("OPENAPI_SPEC", "/app/resources/crapi-openapi-spec.json") + API_USER = os.getenv("API_USER", "admin@example.com") + API_PASSWORD = os.getenv("API_PASSWORD", "Admin!123") \ No newline at end of file diff --git a/services/chatbot/src/mcpserver/server.py b/services/chatbot/src/mcpserver/server.py index 8ea29d46..e0dbc29a 100644 --- a/services/chatbot/src/mcpserver/server.py +++ b/services/chatbot/src/mcpserver/server.py @@ -18,21 +18,17 @@ BASE_URL = f"{'https' if Config.TLS_ENABLED else 'http'}://{Config.WEB_SERVICE}" BASE_IDENTITY_URL = f"{'https' if Config.TLS_ENABLED else 'http'}://{Config.IDENTITY_SERVICE}" -AUTH_TYPE_MAPPING = { - "apikey": "apiKey", - "jwt": "token" -} -CLIENT_AUTH = None +API_KEY = None -def get_client_auth(): - global CLIENT_AUTH +def get_api_key(): + global API_KEY # Try 5 times to get client auth MAX_ATTEMPTS = 5 for i in range(MAX_ATTEMPTS): - logger.info(f"Attempt {i+1} to get client auth...") - if CLIENT_AUTH is None: + logger.info(f"Attempt {i+1} to get API key...") + if API_KEY is None: login_body = {"email": Config.API_USER, "password": Config.API_PASSWORD} - auth_url = f"{BASE_IDENTITY_URL}/identity/management/user/{Config.API_AUTH_TYPE}" + auth_url = f"{BASE_IDENTITY_URL}/identity/management/user/apikey" headers = { "Content-Type": "application/json", } @@ -43,22 +39,22 @@ def get_client_auth(): response = client.post(auth_url, json=login_body) if response.status_code != 200: if i == MAX_ATTEMPTS - 1: - logger.error(f"Failed to get client auth after {i+1} attempts: {response.status_code} {response.text}") - raise Exception(f"Failed to get client auth after {i+1} attempts: {response.status_code} {response.text}") - logger.error(f"Failed to get client auth in attempt {i+1}: {response.status_code} {response.text}. Sleeping for {i} seconds...") + logger.error(f"Failed to get API key after {i+1} attempts: {response.status_code} {response.text}") + raise Exception(f"Failed to get API key after {i+1} attempts: {response.status_code} {response.text}") + logger.error(f"Failed to get API key in attempt {i+1}: {response.status_code} {response.text}. Sleeping for {i} seconds...") time.sleep(i) response_json = response.json() logger.info(f"Response: {response_json}") - CLIENT_AUTH = f"{response_json.get('type')} {response_json.get(AUTH_TYPE_MAPPING[Config.API_AUTH_TYPE])}" - logger.info(f"MCP Server API Auth: {CLIENT_AUTH}") - return CLIENT_AUTH - return CLIENT_AUTH + API_KEY = response_json.get("apiKey") + logger.info(f"MCP Server API Key: {API_KEY}") + return API_KEY + return API_KEY # Async HTTP client for API calls def get_http_client(): """Create and configure the HTTP client with appropriate authentication.""" headers = { - "Authorization": get_client_auth(), + "Authorization": "ApiKey " + get_api_key(), } return httpx.AsyncClient( base_url=BASE_URL, @@ -66,7 +62,7 @@ def get_http_client(): ) # Load your OpenAPI spec -with open("/app/resources/crapi-openapi-spec.json", "r") as f: +with open(Config.OPENAPI_SPEC, "r") as f: openapi_spec = json.load(f) # Create the MCP server diff --git a/services/identity/src/main/java/com/crapi/config/JwtAuthTokenFilter.java b/services/identity/src/main/java/com/crapi/config/JwtAuthTokenFilter.java index a8455283..ddf86cd0 100644 --- a/services/identity/src/main/java/com/crapi/config/JwtAuthTokenFilter.java +++ b/services/identity/src/main/java/com/crapi/config/JwtAuthTokenFilter.java @@ -121,7 +121,7 @@ public String getUserFromToken(HttpServletRequest request) throws ParseException if (token != null) { if (apiType == ApiType.APIKEY) { log.debug("Token is api token"); - username = tokenProvider.getUserNameFromApiToken(token); + username = tokenProvider.getUserNameFromJwtToken(token); } else { log.debug("Token is jwt token"); if (tokenProvider.validateJwtToken(token)) { diff --git a/services/identity/src/main/java/com/crapi/config/JwtProvider.java b/services/identity/src/main/java/com/crapi/config/JwtProvider.java index 17193255..0a84af2a 100644 --- a/services/identity/src/main/java/com/crapi/config/JwtProvider.java +++ b/services/identity/src/main/java/com/crapi/config/JwtProvider.java @@ -103,27 +103,27 @@ public String generateJwtToken(User user) { } /** - * @param token - * @return username from JWT Token + * @param user + * @return generated apikey token without expiry date */ - public String getUserNameFromJwtToken(String token) throws ParseException { - // Parse without verifying token signature - return JWTParser.parse(token).getJWTClaimsSet().getSubject(); + public String generateApiKey(User user) { + JwtBuilder builder = + Jwts.builder() + .subject(user.getEmail()) + .issuedAt(new Date()) + .claim("role", user.getRole().getName()) + .signWith(this.keyPair.getPrivate()); + String jwt = builder.compact(); + return jwt; } /** * @param token * @return username from JWT Token */ - public String getUserNameFromApiToken(String token) throws ParseException { + public String getUserNameFromJwtToken(String token) throws ParseException { // Parse without verifying token signature - if (token != null) { - User user = userRepository.findByApiKey(token); - if (user != null) { - return user.getEmail(); - } - } - return null; + return JWTParser.parse(token).getJWTClaimsSet().getSubject(); } // Load RSA Public Key for JKU header if present diff --git a/services/identity/src/main/java/com/crapi/constant/UserMessage.java b/services/identity/src/main/java/com/crapi/constant/UserMessage.java index e18292a0..f1eead17 100644 --- a/services/identity/src/main/java/com/crapi/constant/UserMessage.java +++ b/services/identity/src/main/java/com/crapi/constant/UserMessage.java @@ -23,9 +23,6 @@ public class UserMessage { "Api Key generated successfully. Use it in authorization header with ApiKey prefix."; public static final String API_KEY_GENERATION_FAILED = "Api Key generation failed! Only permitted for admin users."; - public static final String JWT_TOKEN_GENERATED_MESSAGE = - "JWT Token generated successfully. Use it in authorization header with Bearer prefix."; - public static final String JWT_TOKEN_GENERATION_FAILED = "JWT Token generation failed!"; public static final String ACCOUNT_LOCK_MESSAGE = "User account has been locked."; public static final String ACCOUNT_LOCKED_MESSAGE = "User account is locked. Retry login with MFA to unlock."; diff --git a/services/identity/src/main/java/com/crapi/controller/ManagementControlller.java b/services/identity/src/main/java/com/crapi/controller/ManagementControlller.java index 530c3146..94697315 100644 --- a/services/identity/src/main/java/com/crapi/controller/ManagementControlller.java +++ b/services/identity/src/main/java/com/crapi/controller/ManagementControlller.java @@ -60,12 +60,4 @@ public ResponseEntity generateApiKey( ApiKeyResponse response = userService.generateApiKey(request, loginForm); return ResponseEntity.status(HttpStatus.OK).body(response); } - - @PostMapping("/user/jwt") - public ResponseEntity generateJwt( - @RequestBody LoginForm loginForm, HttpServletRequest request) - throws UnsupportedEncodingException { - JwtResponse response = userService.generateJwtToken(request, loginForm); - return ResponseEntity.status(HttpStatus.OK).body(response); - } } diff --git a/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java b/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java index 056b869c..a32fe6d0 100644 --- a/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java +++ b/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java @@ -25,7 +25,6 @@ import com.crapi.model.*; import com.crapi.repository.*; import com.crapi.service.UserService; -import com.crapi.utils.ApiKeyGenerator; import com.crapi.utils.EmailTokenGenerator; import com.crapi.utils.MailBody; import com.crapi.utils.OTPGenerator; @@ -34,10 +33,13 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; import java.text.ParseException; +import java.util.Arrays; +import java.util.List; import lombok.extern.slf4j.Slf4j; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.impl.Log4jContextFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; @@ -74,6 +76,9 @@ public class UserServiceImpl implements UserService { @Autowired ChangePhoneRepository changePhoneRepository; + @Value("${app.admin.emails}") + private String adminEmails; + public UserServiceImpl() { setFactory(log4jContextFactory); LOG4J_LOGGER = LogManager.getLogger(UserService.class); @@ -486,52 +491,26 @@ public ApiKeyResponse generateApiKey(HttpServletRequest request, LoginForm login log.debug("User not found to generate API key"); return new ApiKeyResponse(null, UserMessage.INVALID_CREDENTIALS); } + List adminEmailList = Arrays.asList(adminEmails.split(",")); + if (!adminEmailList.contains(user.getEmail())) { + log.debug("User {} is not authorized to generate API Key", user.getEmail()); + return new ApiKeyResponse(null, UserMessage.API_KEY_GENERATION_FAILED); + } // if apiKey is already generated if (user.getApiKey() != null) { log.debug("Api Key already generated for user: {}", user.getEmail()); return new ApiKeyResponse(user.getApiKey()); } - String apiKey = ApiKeyGenerator.generateRandom(512); + String apiKey = jwtProvider.generateApiKey(user); log.debug("Api Key for user {}: {}", user.getEmail(), apiKey); + if (apiKey == null) { + return new ApiKeyResponse(null, UserMessage.API_KEY_GENERATION_FAILED); + } user.setApiKey(apiKey); - userRepository.save(user); + userRepository.saveAndFlush(user); return new ApiKeyResponse(user.getApiKey(), UserMessage.API_KEY_GENERATED_MESSAGE); } - /** - * @param request None - * @param loginForm LoginForm with user email and password - * @return JwtResponse with generated JWT token - */ - @Override - @Transactional - public JwtResponse generateJwtToken(HttpServletRequest request, LoginForm loginForm) { - Authentication authentication = - authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(loginForm.getEmail(), loginForm.getPassword())); - if (authentication == null) { - return new JwtResponse(null, UserMessage.INVALID_CREDENTIALS); - } - log.info("Generate JWT token for user: {}", loginForm.getEmail()); - User user; - if (request == null || jwtAuthTokenFilter.getToken(request) == null) { - user = userRepository.findByEmail(loginForm.getEmail()); - } else { - user = getUserFromToken(request); - } - if (user == null) { - log.debug("User not found to generate JWT token"); - return new JwtResponse(null, UserMessage.INVALID_CREDENTIALS); - } - String jwt = jwtProvider.generateJwtToken(user); - log.debug("JWT token for user {}: {}", user.getEmail(), jwt); - if (jwt != null) { - return new JwtResponse(jwt, UserMessage.JWT_TOKEN_GENERATED_MESSAGE); - } else { - return new JwtResponse(null, UserMessage.JWT_TOKEN_GENERATION_FAILED); - } - } - /** * @param changePhoneForm contains old phone number and new phone number, api will send otp to * change number to email address. diff --git a/services/identity/src/main/java/com/crapi/service/UserService.java b/services/identity/src/main/java/com/crapi/service/UserService.java index d645667b..959eca31 100644 --- a/services/identity/src/main/java/com/crapi/service/UserService.java +++ b/services/identity/src/main/java/com/crapi/service/UserService.java @@ -57,6 +57,4 @@ ResponseEntity authenticateUserLogin(LoginForm loginForm) CRAPIResponse lockAccount(HttpServletRequest request, LockAccountForm lockAccountForm); ApiKeyResponse generateApiKey(HttpServletRequest request, LoginForm loginForm); - - JwtResponse generateJwtToken(HttpServletRequest request, LoginForm loginForm); } diff --git a/services/identity/src/main/java/com/crapi/utils/ApiKeyGenerator.java b/services/identity/src/main/java/com/crapi/utils/ApiKeyGenerator.java deleted file mode 100644 index f15167c3..00000000 --- a/services/identity/src/main/java/com/crapi/utils/ApiKeyGenerator.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed 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 - * - * http://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 com.crapi.utils; - -import org.springframework.stereotype.Component; - -@Component -public class ApiKeyGenerator { - - public static String characters = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - - /** - * @param length - * @return generate random otp for forgot password - */ - public static String generateRandom(int length) { - String apiKey = ""; - for (int i = 0; i < length; i++) { - apiKey += randomCharacter(characters); - } - return apiKey; - } - - public static String randomCharacter(String characters) { - int n = characters.length(); - int r = (int) (n * Math.random()); - return characters.substring(r, r + 1); - } -} diff --git a/services/identity/src/main/resources/application.properties b/services/identity/src/main/resources/application.properties index 24bfda57..85ec5897 100644 --- a/services/identity/src/main/resources/application.properties +++ b/services/identity/src/main/resources/application.properties @@ -42,6 +42,7 @@ mail.mailhog.domain=${MAILHOG_DOMAIN} app.enable_shell_injection=${ENABLE_SHELL_INJECTION} app.service.name=crapi-identity +app.admin.emails=${ADMIN_EMAILS} api.gateway.url=${API_GATEWAY_URL} diff --git a/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java b/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java index b8124dde..357386f9 100644 --- a/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java +++ b/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java @@ -66,6 +66,7 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.util.ReflectionTestUtils; @RunWith(MockitoJUnitRunner.class) public class UserServiceImplTest { @@ -157,6 +158,7 @@ public void testAuthenticateUserApiKey() throws UnsupportedEncodingException { Mockito.when(authenticationManager.authenticate(Mockito.any(Authentication.class))) .thenReturn(mockAuth); Mockito.when(userRepository.findByEmail(Mockito.anyString())).thenReturn(user); + ReflectionTestUtils.setField(userService, "adminEmails", user.getEmail()); ApiKeyResponse jwtResponse = userService.generateApiKey(getMockHttpRequest(), loginForm); Assertions.assertEquals(jwtResponse.getApiKey(), "sampleApiKey"); } From 71e314905dd76aa6100767bb58bf6c09ae970a90 Mon Sep 17 00:00:00 2001 From: Keyur Doshi Date: Tue, 29 Jul 2025 10:52:39 +0530 Subject: [PATCH 4/4] Made requested changes --- deploy/docker/docker-compose.yml | 1 - deploy/helm/templates/identity/config.yaml | 1 - deploy/helm/values.yaml | 1 - .../java/com/crapi/service/Impl/UserServiceImpl.java | 11 ----------- .../src/main/resources/application.properties | 1 - .../com/crapi/service/Impl/UserServiceImplTest.java | 2 -- 6 files changed, 17 deletions(-) diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index 9e334079..c82d242b 100755 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -48,7 +48,6 @@ services: - TLS_KEYSTORE_PASSWORD=passw0rd - TLS_KEY_PASSWORD=passw0rd - TLS_KEY_ALIAS=identity - - ADMIN_EMAILS=admin@example.com depends_on: postgresdb: condition: service_healthy diff --git a/deploy/helm/templates/identity/config.yaml b/deploy/helm/templates/identity/config.yaml index 15679897..98046df2 100644 --- a/deploy/helm/templates/identity/config.yaml +++ b/deploy/helm/templates/identity/config.yaml @@ -37,4 +37,3 @@ data: TLS_KEYSTORE_PASSWORD: {{ .Values.identity.config.keyStorePassword }} TLS_KEY_PASSWORD: {{ .Values.identity.config.keyPassword }} TLS_KEY_ALIAS: {{ .Values.identity.config.keyAlias }} - ADMIN_EMAILS: {{ .Values.identity.config.adminEmails | quote }} diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index 71373a17..94864f11 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -77,7 +77,6 @@ identity: keyStorePassword: passw0rd keyPassword: passw0rd keyAlias: identity - adminEmails: admin@example.com deploymentLabels: app: crapi-identity podLabels: diff --git a/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java b/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java index a32fe6d0..40ef7016 100644 --- a/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java +++ b/services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java @@ -33,13 +33,10 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.transaction.Transactional; import java.text.ParseException; -import java.util.Arrays; -import java.util.List; import lombok.extern.slf4j.Slf4j; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.impl.Log4jContextFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; @@ -76,9 +73,6 @@ public class UserServiceImpl implements UserService { @Autowired ChangePhoneRepository changePhoneRepository; - @Value("${app.admin.emails}") - private String adminEmails; - public UserServiceImpl() { setFactory(log4jContextFactory); LOG4J_LOGGER = LogManager.getLogger(UserService.class); @@ -491,11 +485,6 @@ public ApiKeyResponse generateApiKey(HttpServletRequest request, LoginForm login log.debug("User not found to generate API key"); return new ApiKeyResponse(null, UserMessage.INVALID_CREDENTIALS); } - List adminEmailList = Arrays.asList(adminEmails.split(",")); - if (!adminEmailList.contains(user.getEmail())) { - log.debug("User {} is not authorized to generate API Key", user.getEmail()); - return new ApiKeyResponse(null, UserMessage.API_KEY_GENERATION_FAILED); - } // if apiKey is already generated if (user.getApiKey() != null) { log.debug("Api Key already generated for user: {}", user.getEmail()); diff --git a/services/identity/src/main/resources/application.properties b/services/identity/src/main/resources/application.properties index 85ec5897..24bfda57 100644 --- a/services/identity/src/main/resources/application.properties +++ b/services/identity/src/main/resources/application.properties @@ -42,7 +42,6 @@ mail.mailhog.domain=${MAILHOG_DOMAIN} app.enable_shell_injection=${ENABLE_SHELL_INJECTION} app.service.name=crapi-identity -app.admin.emails=${ADMIN_EMAILS} api.gateway.url=${API_GATEWAY_URL} diff --git a/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java b/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java index 357386f9..b8124dde 100644 --- a/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java +++ b/services/identity/src/test/java/com/crapi/service/Impl/UserServiceImplTest.java @@ -66,7 +66,6 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.util.ReflectionTestUtils; @RunWith(MockitoJUnitRunner.class) public class UserServiceImplTest { @@ -158,7 +157,6 @@ public void testAuthenticateUserApiKey() throws UnsupportedEncodingException { Mockito.when(authenticationManager.authenticate(Mockito.any(Authentication.class))) .thenReturn(mockAuth); Mockito.when(userRepository.findByEmail(Mockito.anyString())).thenReturn(user); - ReflectionTestUtils.setField(userService, "adminEmails", user.getEmail()); ApiKeyResponse jwtResponse = userService.generateApiKey(getMockHttpRequest(), loginForm); Assertions.assertEquals(jwtResponse.getApiKey(), "sampleApiKey"); }