Skip to content

Commit 759ed04

Browse files
author
Olivier Gintrand
committed
fix(auth): prioritize per-user credentials over OAuth in tool execution
Per-user personal credentials (API keys, basic auth) stored in user_gateway_credentials were only checked when the gateway did NOT have OAuth configured. For OAuth-enabled gateways (like Freshservice), the OAuth token was always used even when a per-user API key existed. Move the per-user credential lookup BEFORE the OAuth branch so that personal credentials take priority. This fixes write operations on APIs where OAuth tokens lack sufficient permissions but API keys work (e.g. Freshservice PM task updates). Affects both invoke_tool_direct and invoke_tool MCP code paths. Signed-off-by: Olivier Gintrand <olivier.gintrand@forterro.com>
1 parent 5a45e97 commit 759ed04

1 file changed

Lines changed: 48 additions & 46 deletions

File tree

mcpgateway/services/tool_service.py

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3765,7 +3765,28 @@ async def prepare_rust_mcp_tool_execution(
37653765
if not gateway_url:
37663766
return {"eligible": False, "fallbackReason": "missing-gateway-url"}
37673767

3768-
if has_gateway and gateway_auth_type == "oauth" and isinstance(gateway_oauth_config, dict) and gateway_oauth_config:
3768+
# Per-user personal credentials always take priority over
3769+
# gateway-level OAuth tokens.
3770+
user_credential_headers = None
3771+
if has_gateway and app_user_email and gateway_id_str:
3772+
try:
3773+
from mcpgateway.services.credential_storage_service import CredentialStorageService # pylint: disable=import-outside-toplevel
3774+
3775+
with fresh_db_session() as cred_db:
3776+
cred_service = CredentialStorageService(cred_db)
3777+
cred_record = await cred_service.get_credential_record(gateway_id_str, app_user_email)
3778+
if cred_record:
3779+
cred_value = await cred_service.get_credential(gateway_id_str, app_user_email)
3780+
if cred_value:
3781+
user_credential_headers = CredentialStorageService.build_auth_headers(
3782+
cred_record.credential_type, cred_value, gateway_auth_type
3783+
)
3784+
except Exception as e:
3785+
logger.debug(f"Failed to check personal credentials for gateway {gateway_name}: {e}")
3786+
3787+
if user_credential_headers:
3788+
headers = user_credential_headers
3789+
elif has_gateway and gateway_auth_type == "oauth" and isinstance(gateway_oauth_config, dict) and gateway_oauth_config:
37693790
grant_type = gateway_oauth_config.get("grant_type", "client_credentials")
37703791
if grant_type == "authorization_code":
37713792
try:
@@ -3793,28 +3814,8 @@ async def prepare_rust_mcp_tool_execution(
37933814
logger.error(f"Failed to obtain OAuth access token for gateway {gateway_name}: {e}")
37943815
raise ToolInvocationError(f"OAuth authentication failed for gateway: {str(e)}")
37953816
else:
3796-
# Check for per-user personal credentials before falling back to shared gateway auth
3797-
user_credential_headers = None
3798-
if has_gateway and app_user_email and gateway_id_str:
3799-
try:
3800-
from mcpgateway.services.credential_storage_service import CredentialStorageService # pylint: disable=import-outside-toplevel
3801-
3802-
with fresh_db_session() as cred_db:
3803-
cred_service = CredentialStorageService(cred_db)
3804-
cred_record = await cred_service.get_credential_record(gateway_id_str, app_user_email)
3805-
if cred_record:
3806-
cred_value = await cred_service.get_credential(gateway_id_str, app_user_email)
3807-
if cred_value:
3808-
user_credential_headers = CredentialStorageService.build_auth_headers(
3809-
cred_record.credential_type, cred_value, gateway_auth_type
3810-
)
3811-
except Exception as e:
3812-
logger.debug(f"Failed to check personal credentials for gateway {gateway_name}: {e}")
3813-
3814-
if user_credential_headers:
3815-
headers = user_credential_headers
3816-
else:
3817-
headers = decode_auth(gateway_auth_value) if gateway_auth_value else {}
3817+
# No per-user credentials and no OAuth — fall back to shared gateway auth
3818+
headers = decode_auth(gateway_auth_value) if gateway_auth_value else {}
38183819

38193820
if request_headers:
38203821
headers = compute_passthrough_headers_cached(
@@ -4867,7 +4868,28 @@ async def invoke_tool(
48674868

48684869
# Handle OAuth authentication for the gateway (using local variables)
48694870
# NOTE: Use has_gateway instead of gateway to avoid accessing detached ORM object
4870-
if has_gateway and gateway_auth_type == "oauth" and isinstance(gateway_oauth_config, dict) and gateway_oauth_config:
4871+
# Per-user personal credentials always take priority over
4872+
# gateway-level OAuth tokens.
4873+
user_credential_headers = None
4874+
if has_gateway and app_user_email and gateway_id_str:
4875+
try:
4876+
from mcpgateway.services.credential_storage_service import CredentialStorageService # pylint: disable=import-outside-toplevel
4877+
4878+
with fresh_db_session() as cred_db:
4879+
cred_service = CredentialStorageService(cred_db)
4880+
cred_record = await cred_service.get_credential_record(gateway_id_str, app_user_email)
4881+
if cred_record:
4882+
cred_value = await cred_service.get_credential(gateway_id_str, app_user_email)
4883+
if cred_value:
4884+
user_credential_headers = CredentialStorageService.build_auth_headers(
4885+
cred_record.credential_type, cred_value, gateway_auth_type
4886+
)
4887+
except Exception as e:
4888+
logger.debug(f"Failed to check personal credentials for gateway {gateway_name}: {e}")
4889+
4890+
if user_credential_headers:
4891+
headers = user_credential_headers
4892+
elif has_gateway and gateway_auth_type == "oauth" and isinstance(gateway_oauth_config, dict) and gateway_oauth_config:
48714893
grant_type = gateway_oauth_config.get("grant_type", "client_credentials")
48724894

48734895
if grant_type == "authorization_code":
@@ -4903,28 +4925,8 @@ async def invoke_tool(
49034925
logger.error(f"Failed to obtain OAuth access token for gateway {gateway_name}: {e}")
49044926
raise ToolInvocationError(f"OAuth authentication failed for gateway: {str(e)}")
49054927
else:
4906-
# Check for per-user personal credentials before falling back to shared gateway auth
4907-
user_credential_headers = None
4908-
if has_gateway and app_user_email and gateway_id_str:
4909-
try:
4910-
from mcpgateway.services.credential_storage_service import CredentialStorageService # pylint: disable=import-outside-toplevel
4911-
4912-
with fresh_db_session() as cred_db:
4913-
cred_service = CredentialStorageService(cred_db)
4914-
cred_record = await cred_service.get_credential_record(gateway_id_str, app_user_email)
4915-
if cred_record:
4916-
cred_value = await cred_service.get_credential(gateway_id_str, app_user_email)
4917-
if cred_value:
4918-
user_credential_headers = CredentialStorageService.build_auth_headers(
4919-
cred_record.credential_type, cred_value, gateway_auth_type
4920-
)
4921-
except Exception as e:
4922-
logger.debug(f"Failed to check personal credentials for gateway {gateway_name}: {e}")
4923-
4924-
if user_credential_headers:
4925-
headers = user_credential_headers
4926-
else:
4927-
headers = decode_auth(gateway_auth_value) if gateway_auth_value else {}
4928+
# No per-user credentials and no OAuth — fall back to shared gateway auth
4929+
headers = decode_auth(gateway_auth_value) if gateway_auth_value else {}
49284930

49294931
# Use cached passthrough headers (no DB query needed)
49304932
if request_headers:

0 commit comments

Comments
 (0)