diff --git a/mcpgateway/admin.py b/mcpgateway/admin.py index c513761494..47ab649ffb 100644 --- a/mcpgateway/admin.py +++ b/mcpgateway/admin.py @@ -12139,6 +12139,11 @@ async def admin_add_gateway(request: Request, db: Session = Depends(get_db), use if scopes: oauth_config["scopes"] = scopes + # Token endpoint auth method (RFC 6749 Section 2.3) + oauth_token_endpoint_auth_method = str(form.get("oauth_token_endpoint_auth_method", "")) + if oauth_token_endpoint_auth_method: + oauth_config["token_endpoint_auth_method"] = oauth_token_endpoint_auth_method + LOGGER.info(f"✅ Assembled OAuth config from UI form fields: grant_type={oauth_grant_type}, issuer={oauth_issuer}") LOGGER.info(f"DEBUG: Complete oauth_config = {oauth_config}") @@ -12411,6 +12416,11 @@ async def admin_edit_gateway( if scopes: oauth_config["scopes"] = scopes + # Token endpoint auth method (RFC 6749 Section 2.3) + oauth_token_endpoint_auth_method = str(form.get("oauth_token_endpoint_auth_method", "")) + if oauth_token_endpoint_auth_method: + oauth_config["token_endpoint_auth_method"] = oauth_token_endpoint_auth_method + LOGGER.info(f"✅ Assembled OAuth config from UI form fields (edit): grant_type={oauth_grant_type}, issuer={oauth_issuer}") user_email = get_user_email(user) diff --git a/mcpgateway/admin_ui/gateways.js b/mcpgateway/admin_ui/gateways.js index c8028dcf6e..6c42aaa168 100644 --- a/mcpgateway/admin_ui/gateways.js +++ b/mcpgateway/admin_ui/gateways.js @@ -400,6 +400,9 @@ export const editGateway = async function (gatewayId) { const oauthRedirectUriField = safeGetElement("oauth-redirect-uri-gw-edit"); const oauthIssuerField = safeGetElement("oauth-issuer-gw-edit"); const oauthScopesField = safeGetElement("oauth-scopes-gw-edit"); + const oauthTokenEndpointAuthMethodField = safeGetElement( + "oauth-token-endpoint-auth-method-gw-edit" + ); const oauthAuthCodeFields = safeGetElement( "oauth-auth-code-fields-gw-edit" ); @@ -526,6 +529,13 @@ export const editGateway = async function (gatewayId) { ? config.scopes.join(" ") : ""; } + if ( + oauthTokenEndpointAuthMethodField && + config.token_endpoint_auth_method + ) { + oauthTokenEndpointAuthMethodField.value = + config.token_endpoint_auth_method; + } } break; case "query_param": diff --git a/mcpgateway/services/oauth_manager.py b/mcpgateway/services/oauth_manager.py index 3e190aa8f5..8a30f638a6 100644 --- a/mcpgateway/services/oauth_manager.py +++ b/mcpgateway/services/oauth_manager.py @@ -1194,17 +1194,31 @@ async def _exchange_code_for_tokens(self, credentials: Dict[str, Any], code: str token_url = runtime_credentials["token_url"] redirect_uri = runtime_credentials["redirect_uri"] + # Determine token endpoint authentication method (RFC 6749 Section 2.3) + # - "client_secret_post" (default): client_id and client_secret in POST body + # - "client_secret_basic": credentials in Authorization: Basic header + token_auth_method = credentials.get("token_endpoint_auth_method", "client_secret_post") + use_basic_auth = token_auth_method == "client_secret_basic" and client_secret + + # Build HTTP Basic Auth header if required by the provider + auth_header = None + if use_basic_auth: + basic_credentials = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() + auth_header = {"Authorization": f"Basic {basic_credentials}"} + # Prepare token exchange data token_data = { "grant_type": "authorization_code", "code": code, "redirect_uri": redirect_uri, - "client_id": client_id, } - # Only include client_secret if present (public clients don't have secrets) - if client_secret: - token_data["client_secret"] = client_secret + # Include client credentials in POST body only when not using Basic auth + if not use_basic_auth: + token_data["client_id"] = client_id + # Only include client_secret if present (public clients don't have secrets) + if client_secret: + token_data["client_secret"] = client_secret # Add PKCE code_verifier if present (RFC 7636) if code_verifier: @@ -1229,7 +1243,7 @@ async def _exchange_code_for_tokens(self, credentials: Dict[str, Any], code: str for attempt in range(self.max_retries): try: client = await self._get_client() - response = await client.post(token_url, data=token_data, timeout=self.request_timeout) + response = await client.post(token_url, data=token_data, headers=auth_header, timeout=self.request_timeout) response.raise_for_status() # GitHub returns form-encoded responses, not JSON @@ -1294,16 +1308,28 @@ async def refresh_token(self, refresh_token: str, credentials: Dict[str, Any]) - if not client_id: raise OAuthError("No client_id configured for OAuth provider") + # Determine token endpoint authentication method (RFC 6749 Section 2.3) + token_auth_method = credentials.get("token_endpoint_auth_method", "client_secret_post") + use_basic_auth = token_auth_method == "client_secret_basic" and client_secret + + # Build HTTP Basic Auth header if required by the provider + auth_header = None + if use_basic_auth: + basic_credentials = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode() + auth_header = {"Authorization": f"Basic {basic_credentials}"} + # Prepare token refresh request token_data = { "grant_type": "refresh_token", "refresh_token": refresh_token, - "client_id": client_id, } - # Add client_secret if available (some providers require it) - if client_secret: - token_data["client_secret"] = client_secret + # Include client credentials in POST body only when not using Basic auth + if not use_basic_auth: + token_data["client_id"] = client_id + # Add client_secret if available (some providers require it) + if client_secret: + token_data["client_secret"] = client_secret # Add resource parameter for JWT access token (RFC 8707) # Must be included in refresh requests to maintain JWT token type @@ -1324,7 +1350,7 @@ async def refresh_token(self, refresh_token: str, credentials: Dict[str, Any]) - for attempt in range(self.max_retries): try: client = await self._get_client() - response = await client.post(token_url, data=token_data, timeout=self.request_timeout) + response = await client.post(token_url, data=token_data, headers=auth_header, timeout=self.request_timeout) if response.status_code == 200: token_response = response.json() diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index d97241559b..7766f5cae8 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -5965,6 +5965,25 @@
+ How client credentials are sent to the token endpoint (RFC 6749 Section 2.3) +
++ How client credentials are sent to the token endpoint (RFC 6749 Section 2.3) +
+