Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions mcpgateway/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions mcpgateway/admin_ui/gateways.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
Expand Down Expand Up @@ -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":
Expand Down
46 changes: 36 additions & 10 deletions mcpgateway/services/oauth_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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()

Expand Down
38 changes: 38 additions & 0 deletions mcpgateway/templates/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -5965,6 +5965,25 @@ <h3 class="text-lg font-bold mb-4 dark:text-gray-200">
read:user")
</p>
</div>

<div>
<label
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Token Endpoint Auth Method
</label>
<select
name="oauth_token_endpoint_auth_method"
id="oauth-token-endpoint-auth-method-gw"
class="mt-1 px-1.5 block w-full rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-900 dark:text-gray-300"
>
<option value="client_secret_post">client_secret_post (credentials in POST body)</option>
<option value="client_secret_basic">client_secret_basic (HTTP Basic Auth header)</option>
</select>
<p class="mt-1 text-sm text-gray-500">
How client credentials are sent to the token endpoint (RFC 6749 Section 2.3)
</p>
</div>
</div>
</div>

Expand Down Expand Up @@ -10274,6 +10293,25 @@ <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
read:user")
</p>
</div>

<div>
<label
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Token Endpoint Auth Method
</label>
<select
name="oauth_token_endpoint_auth_method"
id="oauth-token-endpoint-auth-method-gw-edit"
class="mt-1 px-1.5 block w-full rounded-md border border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-900 dark:text-gray-300"
>
<option value="client_secret_post">client_secret_post (credentials in POST body)</option>
<option value="client_secret_basic">client_secret_basic (HTTP Basic Auth header)</option>
</select>
<p class="mt-1 text-sm text-gray-500">
How client credentials are sent to the token endpoint (RFC 6749 Section 2.3)
</p>
</div>
</div>
</div>
</div>
Expand Down