Skip to content

Commit 54f9a8a

Browse files
committed
JWT auth added for MCP server's api calls
1 parent ef5f603 commit 54f9a8a

10 files changed

Lines changed: 105 additions & 46 deletions

File tree

deploy/docker/docker-compose.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ services:
156156
environment:
157157
- TLS_ENABLED=${TLS_ENABLED:-false}
158158
- SERVER_PORT=${CHATBOT_SERVER_PORT:-5002}
159+
- WEB_SERVICE=crapi-web
160+
- IDENTITY_SERVICE=crapi-identity:${IDENTITY_SERVER_PORT:-8080}
159161
- DB_NAME=crapi
160162
- DB_USER=admin
161163
- DB_PASSWORD=crapisecretpassword
@@ -166,6 +168,9 @@ services:
166168
- MONGO_DB_USER=admin
167169
- MONGO_DB_PASSWORD=crapisecretpassword
168170
- MONGO_DB_NAME=crapi
171+
- API_USER=admin@example.com
172+
- API_PASSWORD=Admin!123
173+
- API_AUTH_TYPE=jwt
169174
- DEFAULT_MODEL=gpt-4o-mini
170175
- CHROMA_PERSIST_DIRECTORY=/app/vectorstore
171176
# - CHATBOT_OPENAI_API_KEY=

deploy/helm/templates/chatbot/config.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ metadata:
88
data:
99
SERVER_PORT: {{ .Values.chatbot.port | quote }}
1010
IDENTITY_SERVICE: {{ .Values.identity.service.name }}:{{ .Values.identity.port }}
11-
WEB_SERVICE: {{ .Values.web.service.name }}:{{ .Values.web.port }}
11+
WEB_SERVICE: {{ .Values.web.service.name }}
1212
TLS_ENABLED: {{ .Values.tlsEnabled | quote }}
1313
DB_HOST: {{ .Values.postgresdb.service.name }}
1414
DB_USER: {{ .Values.postgresdb.config.postgresUser }}
@@ -23,3 +23,6 @@ data:
2323
CHATBOT_OPENAI_API_KEY: {{ .Values.openAIApiKey }}
2424
DEFAULT_MODEL: {{ .Values.chatbot.config.defaultModel | quote }}
2525
CHROMA_PERSIST_DIRECTORY: {{ .Values.chatbot.config.chromaPersistDirectory | quote }}
26+
API_USER: {{ .Values.chatbot.config.apiUser | quote }}
27+
API_PASSWORD: {{ .Values.chatbot.config.apiPassword | quote }}
28+
API_AUTH_TYPE: {{ .Values.chatbot.config.apiAuthType | quote }}

deploy/helm/values.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ chatbot:
153153
secretKey: crapi
154154
defaultModel: gpt-4o-mini
155155
chromaPersistDirectory: /app/vectorstore
156+
apiUser: admin@example.com
157+
apiPassword: Admin!123
158+
apiAuthType: jwt
156159
storage:
157160
# type: "manual"
158161
# pv:
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import os
2+
from dotenv import load_dotenv
3+
4+
load_dotenv()
5+
6+
class Config:
7+
WEB_SERVICE = os.getenv("WEB_SERVICE", "crapi-web")
8+
IDENTITY_SERVICE = os.getenv("IDENTITY_SERVICE", "crapi-identity:8080")
9+
CHROMA_PERSIST_DIRECTORY = os.getenv("CHROMA_PERSIST_DIRECTORY", "/app/vectorstore")
10+
TLS_ENABLED = os.getenv("TLS_ENABLED", "false").lower() in ("true", "1", "yes")
11+
API_AUTH_TYPE = os.getenv("API_AUTH_TYPE", "jwt") # Toggle between "apikey" and "jwt" based on your auth type

services/chatbot/src/mcpserver/server.py

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from fastmcp import FastMCP, settings
33
import json
44
import os
5+
from .config import Config
56
import logging
67
import time
78
from .tool_helpers import (
@@ -15,58 +16,52 @@
1516
logger = logging.getLogger(__name__)
1617
logger.setLevel(logging.DEBUG)
1718

18-
WEB_SERVICE = os.environ.get("WEB_SERVICE", "crapi-web")
19-
IDENTITY_SERVICE = os.environ.get("IDENTITY_SERVICE", "crapi-identity:8080")
20-
TLS_ENABLED = os.environ.get("TLS_ENABLED", "false").lower() in ("true", "1", "yes")
21-
BASE_URL = f"{'https' if TLS_ENABLED else 'http'}://{WEB_SERVICE}"
22-
BASE_IDENTITY_URL = f"{'https' if TLS_ENABLED else 'http'}://{IDENTITY_SERVICE}"
19+
BASE_URL = f"{'https' if Config.TLS_ENABLED else 'http'}://{Config.WEB_SERVICE}"
20+
BASE_IDENTITY_URL = f"{'https' if Config.TLS_ENABLED else 'http'}://{Config.IDENTITY_SERVICE}"
21+
AUTH_TYPE_MAPPING = {
22+
"apikey": "apiKey",
23+
"jwt": "token"
24+
}
25+
CLIENT_AUTH = None
2326

24-
API_USER = os.environ.get("API_USER", "admin@example.com")
25-
API_PASSWORD = os.environ.get("API_PASSWORD", "Admin!123")
26-
API_URL = f"{'https' if TLS_ENABLED else 'http'}://{WEB_SERVICE}"
27-
28-
API_KEY = None
29-
API_AUTH_TYPE = "ApiKey"
30-
31-
def get_api_key():
32-
global API_KEY
33-
# Try 5 times to get API key
27+
def get_client_auth():
28+
global CLIENT_AUTH
29+
# Try 5 times to get client auth
3430
MAX_ATTEMPTS = 5
3531
for i in range(MAX_ATTEMPTS):
36-
logger.info(f"Attempt {i+1} to get API key...")
37-
if API_KEY is None:
38-
login_body = {"email": API_USER, "password": API_PASSWORD}
39-
apikey_url = f"{BASE_IDENTITY_URL}/identity/management/user/apikey"
32+
logger.info(f"Attempt {i+1} to get client auth...")
33+
if CLIENT_AUTH is None:
34+
login_body = {"email": Config.API_USER, "password": Config.API_PASSWORD}
35+
auth_url = f"{BASE_IDENTITY_URL}/identity/management/user/{Config.API_AUTH_TYPE}"
4036
headers = {
4137
"Content-Type": "application/json",
4238
}
4339
with httpx.Client(
44-
base_url=API_URL,
40+
base_url=BASE_URL,
4541
headers=headers,
4642
) as client:
47-
response = client.post(apikey_url, json=login_body)
43+
response = client.post(auth_url, json=login_body)
4844
if response.status_code != 200:
4945
if i == MAX_ATTEMPTS - 1:
50-
logger.error(f"Failed to get API key after {i+1} attempts: {response.status_code} {response.text}")
51-
raise Exception(f"Failed to get API key after {i+1} attempts: {response.status_code} {response.text}")
52-
logger.error(f"Failed to get API key in attempt {i+1}: {response.status_code} {response.text}. Sleeping for {i} seconds...")
46+
logger.error(f"Failed to get client auth after {i+1} attempts: {response.status_code} {response.text}")
47+
raise Exception(f"Failed to get client auth after {i+1} attempts: {response.status_code} {response.text}")
48+
logger.error(f"Failed to get client auth in attempt {i+1}: {response.status_code} {response.text}. Sleeping for {i} seconds...")
5349
time.sleep(i)
5450
response_json = response.json()
5551
logger.info(f"Response: {response_json}")
56-
API_KEY = response_json.get("apiKey")
57-
logger.info(f"Chatbot API Key: {API_KEY}")
58-
return API_KEY
59-
return API_KEY
60-
52+
CLIENT_AUTH = f"{response_json.get('type')} {response_json.get(AUTH_TYPE_MAPPING[Config.API_AUTH_TYPE])}"
53+
logger.info(f"MCP Server API Auth: {CLIENT_AUTH}")
54+
return CLIENT_AUTH
55+
return CLIENT_AUTH
6156

6257
# Async HTTP client for API calls
6358
def get_http_client():
6459
"""Create and configure the HTTP client with appropriate authentication."""
6560
headers = {
66-
"Authorization": "ApiKey " + get_api_key(),
61+
"Authorization": get_client_auth(),
6762
}
6863
return httpx.AsyncClient(
69-
base_url=API_URL,
64+
base_url=BASE_URL,
7065
headers=headers,
7166
)
7267

services/chatbot/src/mcpserver/tool_helpers.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33
from langchain_community.vectorstores import Chroma
44
from langchain.prompts import PromptTemplate
55
from chatbot.extensions import db
6-
from chatbot.config import Config
6+
from .config import Config
77
from langchain.chains import RetrievalQA
88
from langchain_openai import ChatOpenAI
99

10-
retrieval_index_path = "/app/resources/chat_index"
11-
1210
async def get_any_api_key():
1311
if os.environ.get("CHATBOT_OPENAI_API_KEY"):
1412
return os.environ.get("CHATBOT_OPENAI_API_KEY")

services/identity/src/main/java/com/crapi/constant/UserMessage.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public class UserMessage {
2323
"Api Key generated successfully. Use it in authorization header with ApiKey prefix.";
2424
public static final String API_KEY_GENERATION_FAILED =
2525
"Api Key generation failed! Only permitted for admin users.";
26+
public static final String JWT_TOKEN_GENERATED_MESSAGE =
27+
"JWT Token generated successfully. Use it in authorization header with Bearer prefix.";
28+
public static final String JWT_TOKEN_GENERATION_FAILED = "JWT Token generation failed!";
2629
public static final String ACCOUNT_LOCK_MESSAGE = "User account has been locked.";
2730
public static final String ACCOUNT_LOCKED_MESSAGE =
2831
"User account is locked. Retry login with MFA to unlock.";

services/identity/src/main/java/com/crapi/controller/ManagementControlller.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,12 @@ public ResponseEntity<ApiKeyResponse> generateApiKey(
6060
ApiKeyResponse response = userService.generateApiKey(request, loginForm);
6161
return ResponseEntity.status(HttpStatus.OK).body(response);
6262
}
63+
64+
@PostMapping("/user/jwt")
65+
public ResponseEntity<JwtResponse> generateJwt(
66+
@RequestBody LoginForm loginForm, HttpServletRequest request)
67+
throws UnsupportedEncodingException {
68+
JwtResponse response = userService.generateJwtToken(request, loginForm);
69+
return ResponseEntity.status(HttpStatus.OK).body(response);
70+
}
6371
}

services/identity/src/main/java/com/crapi/service/Impl/UserServiceImpl.java

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -469,19 +469,17 @@ public JwtResponse unlockAccount(
469469
@Override
470470
@Transactional
471471
public ApiKeyResponse generateApiKey(HttpServletRequest request, LoginForm loginForm) {
472-
// if user is unauthenticated, use loginForm else user token to authenticate
472+
Authentication authentication =
473+
authenticationManager.authenticate(
474+
new UsernamePasswordAuthenticationToken(loginForm.getEmail(), loginForm.getPassword()));
475+
if (authentication == null) {
476+
return new ApiKeyResponse(null, UserMessage.INVALID_CREDENTIALS);
477+
}
478+
log.info("Generate Api Key for user: {}", loginForm.getEmail());
473479
User user;
474480
if (request == null || jwtAuthTokenFilter.getToken(request) == null) {
475481
user = userRepository.findByEmail(loginForm.getEmail());
476482
} else {
477-
log.info("Generate Api Key for user: {}", loginForm.getEmail());
478-
Authentication authentication =
479-
authenticationManager.authenticate(
480-
new UsernamePasswordAuthenticationToken(
481-
loginForm.getEmail(), loginForm.getPassword()));
482-
if (authentication == null) {
483-
return new ApiKeyResponse(null, UserMessage.INVALID_CREDENTIALS);
484-
}
485483
user = getUserFromToken(request);
486484
}
487485
if (user == null) {
@@ -493,14 +491,47 @@ public ApiKeyResponse generateApiKey(HttpServletRequest request, LoginForm login
493491
log.debug("Api Key already generated for user: {}", user.getEmail());
494492
return new ApiKeyResponse(user.getApiKey());
495493
}
496-
log.info("Generate Api Key for user in token: {}", user.getEmail());
497494
String apiKey = ApiKeyGenerator.generateRandom(512);
498-
log.debug("Api Key for user in token {}: {}", user.getEmail(), apiKey);
495+
log.debug("Api Key for user {}: {}", user.getEmail(), apiKey);
499496
user.setApiKey(apiKey);
500497
userRepository.save(user);
501498
return new ApiKeyResponse(user.getApiKey(), UserMessage.API_KEY_GENERATED_MESSAGE);
502499
}
503500

501+
/**
502+
* @param request None
503+
* @param loginForm LoginForm with user email and password
504+
* @return JwtResponse with generated JWT token
505+
*/
506+
@Override
507+
@Transactional
508+
public JwtResponse generateJwtToken(HttpServletRequest request, LoginForm loginForm) {
509+
Authentication authentication =
510+
authenticationManager.authenticate(
511+
new UsernamePasswordAuthenticationToken(loginForm.getEmail(), loginForm.getPassword()));
512+
if (authentication == null) {
513+
return new JwtResponse(null, UserMessage.INVALID_CREDENTIALS);
514+
}
515+
log.info("Generate JWT token for user: {}", loginForm.getEmail());
516+
User user;
517+
if (request == null || jwtAuthTokenFilter.getToken(request) == null) {
518+
user = userRepository.findByEmail(loginForm.getEmail());
519+
} else {
520+
user = getUserFromToken(request);
521+
}
522+
if (user == null) {
523+
log.debug("User not found to generate JWT token");
524+
return new JwtResponse(null, UserMessage.INVALID_CREDENTIALS);
525+
}
526+
String jwt = jwtProvider.generateJwtToken(user);
527+
log.debug("JWT token for user {}: {}", user.getEmail(), jwt);
528+
if (jwt != null) {
529+
return new JwtResponse(jwt, UserMessage.JWT_TOKEN_GENERATED_MESSAGE);
530+
} else {
531+
return new JwtResponse(null, UserMessage.JWT_TOKEN_GENERATION_FAILED);
532+
}
533+
}
534+
504535
/**
505536
* @param changePhoneForm contains old phone number and new phone number, api will send otp to
506537
* change number to email address.

services/identity/src/main/java/com/crapi/service/UserService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,6 @@ ResponseEntity<JwtResponse> authenticateUserLogin(LoginForm loginForm)
5757
CRAPIResponse lockAccount(HttpServletRequest request, LockAccountForm lockAccountForm);
5858

5959
ApiKeyResponse generateApiKey(HttpServletRequest request, LoginForm loginForm);
60+
61+
JwtResponse generateJwtToken(HttpServletRequest request, LoginForm loginForm);
6062
}

0 commit comments

Comments
 (0)