Skip to content

Commit b40d321

Browse files
Fix jti consistency for token creation (#1255)
* jti consistency for token creation Signed-off-by: Keval Mahajan <[email protected]> * linting Signed-off-by: Keval Mahajan <[email protected]> * updated test cases Signed-off-by: Keval Mahajan <[email protected]> * rebase Signed-off-by: Mihai Criveti <[email protected]> --------- Signed-off-by: Keval Mahajan <[email protected]> Signed-off-by: Mihai Criveti <[email protected]> Co-authored-by: Mihai Criveti <[email protected]>
1 parent 6e2434b commit b40d321

File tree

2 files changed

+21
-11
lines changed

2 files changed

+21
-11
lines changed

mcpgateway/services/token_catalog_service.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,9 @@ def __init__(self, db: Session):
213213
"""
214214
self.db = db
215215

216-
async def _generate_token(self, user_email: str, team_id: Optional[str] = None, expires_at: Optional[datetime] = None, scope: Optional["TokenScope"] = None, user: Optional[object] = None) -> str:
216+
async def _generate_token(
217+
self, user_email: str, jti: str, team_id: Optional[str] = None, expires_at: Optional[datetime] = None, scope: Optional["TokenScope"] = None, user: Optional[object] = None
218+
) -> str:
217219
"""Generate a JWT token for API access.
218220
219221
This internal method creates a properly formatted JWT token with all
@@ -222,6 +224,7 @@ async def _generate_token(self, user_email: str, team_id: Optional[str] = None,
222224
223225
Args:
224226
user_email: User's email address for the token subject
227+
jti: JWT ID for token uniqueness
225228
team_id: Optional team ID for team-scoped tokens
226229
expires_at: Optional expiration datetime
227230
scope: Optional token scope information for access control
@@ -242,7 +245,7 @@ async def _generate_token(self, user_email: str, team_id: Optional[str] = None,
242245
"iss": settings.jwt_issuer, # Issuer
243246
"aud": settings.jwt_audience, # Audience
244247
"iat": int(now.timestamp()), # Issued at
245-
"jti": str(uuid.uuid4()), # JWT ID for uniqueness
248+
"jti": jti, # JWT ID for uniqueness
246249
"user": {"email": user_email, "full_name": "API Token User", "is_admin": user.is_admin if user else False, "auth_provider": "api_token"}, # Use actual admin status if user provided
247250
"teams": [team_id] if team_id else [],
248251
"namespaces": [f"user:{user_email}", "public"] + ([f"team:{team_id}"] if team_id else []),
@@ -383,8 +386,9 @@ async def create_token(
383386
if expires_in_days:
384387
expires_at = utc_now() + timedelta(days=expires_in_days)
385388

389+
jti = str(uuid.uuid4()) # Unique JWT ID
386390
# Generate JWT token with all necessary claims
387-
raw_token = await self._generate_token(user_email=user_email, team_id=team_id, expires_at=expires_at, scope=scope, user=user) # Pass user object to include admin status
391+
raw_token = await self._generate_token(user_email=user_email, jti=jti, team_id=team_id, expires_at=expires_at, scope=scope, user=user) # Pass user object to include admin status
388392

389393
# Hash token for secure storage
390394
token_hash = self._hash_token(raw_token)
@@ -395,6 +399,7 @@ async def create_token(
395399
user_email=user_email,
396400
team_id=team_id, # Store team association
397401
name=name,
402+
jti=jti,
398403
description=description,
399404
token_hash=token_hash, # Store hash, not raw token
400405
expires_at=expires_at,

tests/unit/mcpgateway/services/test_token_catalog_service.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,8 @@ async def test_generate_token_basic(self, token_service):
237237
"""Test _generate_token method with basic parameters."""
238238
with patch("mcpgateway.services.token_catalog_service.create_jwt_token", new_callable=AsyncMock) as mock_create_jwt:
239239
mock_create_jwt.return_value = "jwt_token_123"
240-
241-
token = await token_service._generate_token("[email protected]")
240+
jti=str(uuid.uuid4())
241+
token = await token_service._generate_token("[email protected]",jti)
242242

243243
assert token == "jwt_token_123"
244244
mock_create_jwt.assert_called_once()
@@ -253,8 +253,8 @@ async def test_generate_token_with_team(self, token_service):
253253
"""Test _generate_token method with team_id."""
254254
with patch("mcpgateway.services.token_catalog_service.create_jwt_token", new_callable=AsyncMock) as mock_create_jwt:
255255
mock_create_jwt.return_value = "jwt_token_team"
256-
257-
token = await token_service._generate_token("[email protected]", team_id="team-123")
256+
jti=str(uuid.uuid4())
257+
token = await token_service._generate_token("[email protected]", jti=jti, team_id="team-123")
258258

259259
assert token == "jwt_token_team"
260260
call_args = mock_create_jwt.call_args[0][0]
@@ -267,8 +267,9 @@ async def test_generate_token_with_expiry(self, token_service):
267267
with patch("mcpgateway.services.token_catalog_service.create_jwt_token", new_callable=AsyncMock) as mock_create_jwt:
268268
mock_create_jwt.return_value = "jwt_token_exp"
269269
expires_at = datetime.now(timezone.utc) + timedelta(days=7)
270+
jti=str(uuid.uuid4())
270271

271-
token = await token_service._generate_token("[email protected]", expires_at=expires_at)
272+
token = await token_service._generate_token("[email protected]", jti=jti, expires_at=expires_at)
272273

273274
assert token == "jwt_token_exp"
274275
call_args = mock_create_jwt.call_args[0][0]
@@ -280,8 +281,9 @@ async def test_generate_token_with_scope(self, token_service, token_scope):
280281
"""Test _generate_token method with TokenScope."""
281282
with patch("mcpgateway.services.token_catalog_service.create_jwt_token", new_callable=AsyncMock) as mock_create_jwt:
282283
mock_create_jwt.return_value = "jwt_token_scoped"
284+
jti=str(uuid.uuid4())
283285

284-
token = await token_service._generate_token("[email protected]", scope=token_scope)
286+
token = await token_service._generate_token("[email protected]", jti=jti, scope=token_scope)
285287

286288
assert token == "jwt_token_scoped"
287289
call_args = mock_create_jwt.call_args[0][0]
@@ -295,8 +297,9 @@ async def test_generate_token_with_admin_user(self, token_service, mock_user):
295297
mock_user.is_admin = True
296298
with patch("mcpgateway.services.token_catalog_service.create_jwt_token", new_callable=AsyncMock) as mock_create_jwt:
297299
mock_create_jwt.return_value = "jwt_token_admin"
300+
jti=str(uuid.uuid4())
298301

299-
token = await token_service._generate_token("[email protected]", user=mock_user)
302+
token = await token_service._generate_token("[email protected]", jti=jti, user=mock_user)
300303

301304
assert token == "jwt_token_admin"
302305
call_args = mock_create_jwt.call_args[0][0]
@@ -918,7 +921,9 @@ async def test_generate_token_settings_values(self, token_service):
918921

919922
with patch("mcpgateway.services.token_catalog_service.create_jwt_token", new_callable=AsyncMock) as mock_create:
920923
mock_create.return_value = "jwt"
921-
await token_service._generate_token("[email protected]")
924+
jti=str(uuid.uuid4())
925+
926+
await token_service._generate_token("[email protected]", jti=jti)
922927

923928
call_args = mock_create.call_args[0][0]
924929
assert call_args["iss"] == "test-issuer"

0 commit comments

Comments
 (0)