Skip to content

Commit 85e520b

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 06746db commit 85e520b

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
@@ -3761,7 +3761,28 @@ async def prepare_rust_mcp_tool_execution(
37613761
if not gateway_url:
37623762
return {"eligible": False, "fallbackReason": "missing-gateway-url"}
37633763

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

38153816
if request_headers:
38163817
headers = compute_passthrough_headers_cached(
@@ -4862,7 +4863,28 @@ async def invoke_tool(
48624863

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

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

49244926
# Use cached passthrough headers (no DB query needed)
49254927
if request_headers:

0 commit comments

Comments
 (0)