Skip to content

Commit b0004f6

Browse files
committed
add configurable logging, bearer token logging
1 parent 72a261a commit b0004f6

4 files changed

Lines changed: 49 additions & 6 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,6 @@ UPDATE_INDEX_ENDPOINT_TYPE="all"
6363

6464
# Periodic refresh interval in minutes (0 to disable)
6565
REFRESH_INTERVAL_MINUTES="5"
66+
67+
# Options: "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
68+
LOG_LEVEL="INFO"

src/mcp_server_obp/auth.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,14 @@ async def verify_token(self, token: str):
178178
AccessToken object if valid, None if invalid or expired
179179
"""
180180
logger.info("🔐 Starting token verification")
181-
logger.debug(f"Token (first 20 chars): {token[:20]}...")
182-
logger.debug(f"Configured issuers: {list(self._verifiers.keys())}")
181+
logger.info(f"Token (first 20 chars): {token[:20]}...")
182+
logger.info(f"Configured issuers: {list(self._verifiers.keys())}")
183+
184+
# Fast-fail on clearly malformed tokens to avoid noisy verifier errors
185+
token = token.strip()
186+
if token.count(".") < 2:
187+
logger.warning("Rejected token without JWT structure (expected header.payload.signature)")
188+
return None
183189

184190
# Extract issuer from token
185191
issuer = self._extract_issuer(token)

src/mcp_server_obp/server.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
# Load environment variables from .env file before anything else
1111
load_dotenv()
1212

13-
# Configure logging
13+
# Configure logging (allow override via LOG_LEVEL env)
14+
_log_level = os.getenv("LOG_LEVEL", "INFO").upper()
1415
logging.basicConfig(
15-
level=logging.INFO,
16+
level=getattr(logging, _log_level, logging.INFO),
1617
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
1718
)
19+
# Ensure our package logger honors the chosen level (basicConfig locks in handlers)
20+
logging.getLogger("mcp_server_obp").setLevel(getattr(logging, _log_level, logging.INFO))
1821

1922
# Add the src and project root directories to the Python path when running as a script
2023
_src_dir = Path(__file__).parent.parent
@@ -204,7 +207,7 @@ async def call_obp_api(
204207
url = f"{base_url}{path}"
205208

206209
# Prepare request
207-
method = endpoint.method.value # HttpMethod enum value
210+
method = endpoint.method # HttpMethod enum value
208211
request_headers = headers or {}
209212

210213
auth_method = os.getenv("OBP_AUTHORIZATION_VIA", "none").lower()
@@ -224,7 +227,7 @@ async def call_obp_api(
224227
"error": "consent_required",
225228
"endpoint_id": endpoint_id,
226229
"operation_id": endpoint.operation_id,
227-
"method": endpoint.method.value,
230+
"method": endpoint.method,
228231
"path": endpoint.path,
229232
"required_roles": [role.model_dump() for role in endpoint.roles],
230233
"message": f"User consent is required to call {endpoint.operation_id}. "
@@ -263,8 +266,12 @@ async def call_obp_api(
263266

264267
try:
265268
result["response"] = response.json()
269+
logging.info(f"OBP API call successful: {method} {url} - Status: {response.status_code}")
270+
logging.info(f"Response: {json.dumps(result['response'], indent=2)[:500]}...") # Log first 500 chars of response
266271
except:
267272
result["response"] = response.text
273+
logging.info(f"OBP API call with non-JSON response: {method} {url} - Status: {response.status_code}")
274+
logging.info(f"Response: {result['response'][:500]}...") # Log first 500 chars of response
268275

269276
return json.dumps(result, indent=2)
270277

tests/test_auth.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,33 @@ async def test_verify_token_unknown_issuer_tries_all(self, keycloak_realm_url, o
207207
mock_verifier1.verify_token.assert_called_once_with(token)
208208
mock_verifier2.verify_token.assert_called_once_with(token)
209209

210+
@pytest.mark.asyncio
211+
async def test_verify_token_rejects_non_jwt_without_verifiers(self, keycloak_realm_url):
212+
"""Ensure malformed tokens fail fast without hitting any verifier."""
213+
bad_token = "not-a-jwt"
214+
215+
with patch("src.mcp_server_obp.auth.JWTVerifier") as MockJWTVerifier:
216+
mock_verifier = AsyncMock()
217+
mock_verifier.verify_token = AsyncMock(return_value={"sub": "user123"})
218+
MockJWTVerifier.return_value = mock_verifier
219+
220+
verifier = MultiIssuerJWTVerifier(
221+
issuers=[
222+
IssuerConfig(
223+
issuer=keycloak_realm_url,
224+
jwks_uri=f"{keycloak_realm_url}/certs",
225+
)
226+
]
227+
)
228+
229+
# Replace the verifier with our mock
230+
verifier._verifiers[keycloak_realm_url] = mock_verifier
231+
232+
result = await verifier.verify_token(bad_token)
233+
234+
assert result is None
235+
mock_verifier.verify_token.assert_not_called()
236+
210237

211238
class TestCreateBearerAuth:
212239
"""Tests for create_bearer_auth() function."""

0 commit comments

Comments
 (0)