Skip to content

Commit 51f376c

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 27c3545 commit 51f376c

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
@@ -3919,7 +3919,28 @@ async def prepare_rust_mcp_tool_execution(
39193919
# plugin invocation enforces the requirement locally with an actionable error.
39203920
oauth_authcode_no_db_token = False
39213921

3922-
if has_gateway and gateway_auth_type == "oauth" and isinstance(gateway_oauth_config, dict) and gateway_oauth_config:
3922+
# Per-user personal credentials always take priority over
3923+
# gateway-level OAuth tokens.
3924+
user_credential_headers = None
3925+
if has_gateway and app_user_email and gateway_id_str:
3926+
try:
3927+
from mcpgateway.services.credential_storage_service import CredentialStorageService # pylint: disable=import-outside-toplevel
3928+
3929+
with fresh_db_session() as cred_db:
3930+
cred_service = CredentialStorageService(cred_db)
3931+
cred_record = await cred_service.get_credential_record(gateway_id_str, app_user_email)
3932+
if cred_record:
3933+
cred_value = await cred_service.get_credential(gateway_id_str, app_user_email)
3934+
if cred_value:
3935+
user_credential_headers = CredentialStorageService.build_auth_headers(
3936+
cred_record.credential_type, cred_value, gateway_auth_type
3937+
)
3938+
except Exception as e:
3939+
logger.debug(f"Failed to check personal credentials for gateway {gateway_name}: {e}")
3940+
3941+
if user_credential_headers:
3942+
headers = user_credential_headers
3943+
elif has_gateway and gateway_auth_type == "oauth" and isinstance(gateway_oauth_config, dict) and gateway_oauth_config:
39233944
grant_type = gateway_oauth_config.get("grant_type", "client_credentials")
39243945
if grant_type == "authorization_code":
39253946
try:
@@ -3957,28 +3978,8 @@ async def prepare_rust_mcp_tool_execution(
39573978
logger.error(f"Failed to obtain OAuth access token for gateway {gateway_name}: {e}")
39583979
raise ToolInvocationError(f"OAuth authentication failed for gateway: {str(e)}")
39593980
else:
3960-
# Check for per-user personal credentials before falling back to shared gateway auth
3961-
user_credential_headers = None
3962-
if has_gateway and app_user_email and gateway_id_str:
3963-
try:
3964-
from mcpgateway.services.credential_storage_service import CredentialStorageService # pylint: disable=import-outside-toplevel
3965-
3966-
with fresh_db_session() as cred_db:
3967-
cred_service = CredentialStorageService(cred_db)
3968-
cred_record = await cred_service.get_credential_record(gateway_id_str, app_user_email)
3969-
if cred_record:
3970-
cred_value = await cred_service.get_credential(gateway_id_str, app_user_email)
3971-
if cred_value:
3972-
user_credential_headers = CredentialStorageService.build_auth_headers(
3973-
cred_record.credential_type, cred_value, gateway_auth_type
3974-
)
3975-
except Exception as e:
3976-
logger.debug(f"Failed to check personal credentials for gateway {gateway_name}: {e}")
3977-
3978-
if user_credential_headers:
3979-
headers = user_credential_headers
3980-
else:
3981-
headers = decode_auth(gateway_auth_value) if gateway_auth_value else {}
3981+
# No per-user credentials and no OAuth — fall back to shared gateway auth
3982+
headers = decode_auth(gateway_auth_value) if gateway_auth_value else {}
39823983

39833984
if request_headers:
39843985
headers = compute_passthrough_headers_cached(
@@ -5071,7 +5072,28 @@ async def invoke_tool(
50715072

50725073
# Handle OAuth authentication for the gateway (using local variables)
50735074
# NOTE: Use has_gateway instead of gateway to avoid accessing detached ORM object
5074-
if has_gateway and gateway_auth_type == "oauth" and isinstance(gateway_oauth_config, dict) and gateway_oauth_config:
5075+
# Per-user personal credentials always take priority over
5076+
# gateway-level OAuth tokens.
5077+
user_credential_headers = None
5078+
if has_gateway and app_user_email and gateway_id_str:
5079+
try:
5080+
from mcpgateway.services.credential_storage_service import CredentialStorageService # pylint: disable=import-outside-toplevel
5081+
5082+
with fresh_db_session() as cred_db:
5083+
cred_service = CredentialStorageService(cred_db)
5084+
cred_record = await cred_service.get_credential_record(gateway_id_str, app_user_email)
5085+
if cred_record:
5086+
cred_value = await cred_service.get_credential(gateway_id_str, app_user_email)
5087+
if cred_value:
5088+
user_credential_headers = CredentialStorageService.build_auth_headers(
5089+
cred_record.credential_type, cred_value, gateway_auth_type
5090+
)
5091+
except Exception as e:
5092+
logger.debug(f"Failed to check personal credentials for gateway {gateway_name}: {e}")
5093+
5094+
if user_credential_headers:
5095+
headers = user_credential_headers
5096+
elif has_gateway and gateway_auth_type == "oauth" and isinstance(gateway_oauth_config, dict) and gateway_oauth_config:
50755097
grant_type = gateway_oauth_config.get("grant_type", "client_credentials")
50765098

50775099
if grant_type == "authorization_code":
@@ -5118,28 +5140,8 @@ async def invoke_tool(
51185140
logger.error(f"Failed to obtain OAuth access token for gateway {gateway_name}: {e}")
51195141
raise ToolInvocationError(f"OAuth authentication failed for gateway: {str(e)}")
51205142
else:
5121-
# Check for per-user personal credentials before falling back to shared gateway auth
5122-
user_credential_headers = None
5123-
if has_gateway and app_user_email and gateway_id_str:
5124-
try:
5125-
from mcpgateway.services.credential_storage_service import CredentialStorageService # pylint: disable=import-outside-toplevel
5126-
5127-
with fresh_db_session() as cred_db:
5128-
cred_service = CredentialStorageService(cred_db)
5129-
cred_record = await cred_service.get_credential_record(gateway_id_str, app_user_email)
5130-
if cred_record:
5131-
cred_value = await cred_service.get_credential(gateway_id_str, app_user_email)
5132-
if cred_value:
5133-
user_credential_headers = CredentialStorageService.build_auth_headers(
5134-
cred_record.credential_type, cred_value, gateway_auth_type
5135-
)
5136-
except Exception as e:
5137-
logger.debug(f"Failed to check personal credentials for gateway {gateway_name}: {e}")
5138-
5139-
if user_credential_headers:
5140-
headers = user_credential_headers
5141-
else:
5142-
headers = decode_auth(gateway_auth_value) if gateway_auth_value else {}
5143+
# No per-user credentials and no OAuth — fall back to shared gateway auth
5144+
headers = decode_auth(gateway_auth_value) if gateway_auth_value else {}
51435145

51445146
# Use cached passthrough headers (no DB query needed)
51455147
if request_headers:

0 commit comments

Comments
 (0)