Skip to content

Commit 7b9e9e2

Browse files
author
Olivier Gintrand
committed
feat: support client_secret_basic token endpoint auth method (RFC 6749)
Add token_endpoint_auth_method support to OAuth token exchange and refresh. When set to 'client_secret_basic' in oauth_config, client credentials are sent via HTTP Basic Auth header instead of POST body. This is required by providers like Freshworks that only accept Basic auth at their token endpoint. Default behavior (client_secret_post) is unchanged.
1 parent c04f65e commit 7b9e9e2

1 file changed

Lines changed: 36 additions & 10 deletions

File tree

mcpgateway/services/oauth_manager.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,17 +1194,31 @@ async def _exchange_code_for_tokens(self, credentials: Dict[str, Any], code: str
11941194
token_url = runtime_credentials["token_url"]
11951195
redirect_uri = runtime_credentials["redirect_uri"]
11961196

1197+
# Determine token endpoint authentication method (RFC 6749 Section 2.3)
1198+
# - "client_secret_post" (default): client_id and client_secret in POST body
1199+
# - "client_secret_basic": credentials in Authorization: Basic header
1200+
token_auth_method = credentials.get("token_endpoint_auth_method", "client_secret_post")
1201+
use_basic_auth = token_auth_method == "client_secret_basic" and client_secret
1202+
1203+
# Build HTTP Basic Auth header if required by the provider
1204+
auth_header = None
1205+
if use_basic_auth:
1206+
basic_credentials = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
1207+
auth_header = {"Authorization": f"Basic {basic_credentials}"}
1208+
11971209
# Prepare token exchange data
11981210
token_data = {
11991211
"grant_type": "authorization_code",
12001212
"code": code,
12011213
"redirect_uri": redirect_uri,
1202-
"client_id": client_id,
12031214
}
12041215

1205-
# Only include client_secret if present (public clients don't have secrets)
1206-
if client_secret:
1207-
token_data["client_secret"] = client_secret
1216+
# Include client credentials in POST body only when not using Basic auth
1217+
if not use_basic_auth:
1218+
token_data["client_id"] = client_id
1219+
# Only include client_secret if present (public clients don't have secrets)
1220+
if client_secret:
1221+
token_data["client_secret"] = client_secret
12081222

12091223
# Add PKCE code_verifier if present (RFC 7636)
12101224
if code_verifier:
@@ -1229,7 +1243,7 @@ async def _exchange_code_for_tokens(self, credentials: Dict[str, Any], code: str
12291243
for attempt in range(self.max_retries):
12301244
try:
12311245
client = await self._get_client()
1232-
response = await client.post(token_url, data=token_data, timeout=self.request_timeout)
1246+
response = await client.post(token_url, data=token_data, headers=auth_header, timeout=self.request_timeout)
12331247
response.raise_for_status()
12341248

12351249
# GitHub returns form-encoded responses, not JSON
@@ -1294,16 +1308,28 @@ async def refresh_token(self, refresh_token: str, credentials: Dict[str, Any]) -
12941308
if not client_id:
12951309
raise OAuthError("No client_id configured for OAuth provider")
12961310

1311+
# Determine token endpoint authentication method (RFC 6749 Section 2.3)
1312+
token_auth_method = credentials.get("token_endpoint_auth_method", "client_secret_post")
1313+
use_basic_auth = token_auth_method == "client_secret_basic" and client_secret
1314+
1315+
# Build HTTP Basic Auth header if required by the provider
1316+
auth_header = None
1317+
if use_basic_auth:
1318+
basic_credentials = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
1319+
auth_header = {"Authorization": f"Basic {basic_credentials}"}
1320+
12971321
# Prepare token refresh request
12981322
token_data = {
12991323
"grant_type": "refresh_token",
13001324
"refresh_token": refresh_token,
1301-
"client_id": client_id,
13021325
}
13031326

1304-
# Add client_secret if available (some providers require it)
1305-
if client_secret:
1306-
token_data["client_secret"] = client_secret
1327+
# Include client credentials in POST body only when not using Basic auth
1328+
if not use_basic_auth:
1329+
token_data["client_id"] = client_id
1330+
# Add client_secret if available (some providers require it)
1331+
if client_secret:
1332+
token_data["client_secret"] = client_secret
13071333

13081334
# Add resource parameter for JWT access token (RFC 8707)
13091335
# 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]) -
13241350
for attempt in range(self.max_retries):
13251351
try:
13261352
client = await self._get_client()
1327-
response = await client.post(token_url, data=token_data, timeout=self.request_timeout)
1353+
response = await client.post(token_url, data=token_data, headers=auth_header, timeout=self.request_timeout)
13281354
if response.status_code == 200:
13291355
token_response = response.json()
13301356

0 commit comments

Comments
 (0)