Skip to content

Commit 7930688

Browse files
author
Olivier Gintrand
committed
feat(oauth): support client_secret_basic token endpoint auth method (RFC 6749)
Signed-off-by: Olivier Gintrand <olivier.gintrand@forterro.com>
1 parent a02a04b commit 7930688

File tree

6 files changed

+108
-19
lines changed

6 files changed

+108
-19
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2136,6 +2136,11 @@ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
21362136
# TOOL_CONCURRENT_LIMIT=10
21372137
# GATEWAY_TOOL_NAME_SEPARATOR=-
21382138

2139+
# Maximum length of response text returned for non-JSON REST API responses
2140+
# Longer responses are truncated to prevent exposing excessive sensitive data
2141+
# Default: 5000 characters, Range: 1000-100000
2142+
# REST_RESPONSE_TEXT_MAX_LENGTH=5000
2143+
21392144
# Prompt Configuration
21402145
# PROMPT_CACHE_SIZE=100
21412146
# MAX_PROMPT_SIZE=102400

.secrets.baseline

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "^.secrets.baseline|package-lock.json|Cargo.lock|scripts/sign_image.sh|scripts/zap|sonar-project.properties|uv.lock|go.sum|mcpgateway/sri_hashes.json|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2026-04-14T13:09:46Z",
6+
"generated_at": "2026-04-14T14:08:10Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -4830,7 +4830,7 @@
48304830
"hashed_secret": "ff37a98a9963d347e9749a5c1b3936a4a245a6ff",
48314831
"is_secret": false,
48324832
"is_verified": false,
4833-
"line_number": 2228,
4833+
"line_number": 2236,
48344834
"type": "Secret Keyword",
48354835
"verified_result": null
48364836
}
@@ -8624,39 +8624,39 @@
86248624
"hashed_secret": "ee977806d7286510da8b9a7492ba58e2484c0ecc",
86258625
"is_secret": false,
86268626
"is_verified": false,
8627-
"line_number": 6376,
8627+
"line_number": 6907,
86288628
"type": "Secret Keyword",
86298629
"verified_result": null
86308630
},
86318631
{
86328632
"hashed_secret": "f2e7745f43b0ef0e2c2faf61d6c6a28be2965750",
86338633
"is_secret": false,
86348634
"is_verified": false,
8635-
"line_number": 6868,
8635+
"line_number": 7399,
86368636
"type": "Secret Keyword",
86378637
"verified_result": null
86388638
},
86398639
{
86408640
"hashed_secret": "4a249743d4d2241bd2ae085b4fe654d089488295",
86418641
"is_secret": false,
86428642
"is_verified": false,
8643-
"line_number": 8215,
8643+
"line_number": 8746,
86448644
"type": "Secret Keyword",
86458645
"verified_result": null
86468646
},
86478647
{
86488648
"hashed_secret": "0c8d051d3c7eada5d31b53d9936fce6bcc232ae2",
86498649
"is_secret": false,
86508650
"is_verified": false,
8651-
"line_number": 8357,
8651+
"line_number": 8888,
86528652
"type": "Secret Keyword",
86538653
"verified_result": null
86548654
},
86558655
{
86568656
"hashed_secret": "f2b14f68eb995facb3a1c35287b778d5bd785511",
86578657
"is_secret": false,
86588658
"is_verified": false,
8659-
"line_number": 8733,
8659+
"line_number": 9264,
86608660
"type": "Secret Keyword",
86618661
"verified_result": null
86628662
}

mcpgateway/admin.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12139,6 +12139,11 @@ async def admin_add_gateway(request: Request, db: Session = Depends(get_db), use
1213912139
if scopes:
1214012140
oauth_config["scopes"] = scopes
1214112141

12142+
# Token endpoint auth method (RFC 6749 Section 2.3)
12143+
oauth_token_endpoint_auth_method = str(form.get("oauth_token_endpoint_auth_method", ""))
12144+
if oauth_token_endpoint_auth_method:
12145+
oauth_config["token_endpoint_auth_method"] = oauth_token_endpoint_auth_method
12146+
1214212147
LOGGER.info(f"✅ Assembled OAuth config from UI form fields: grant_type={oauth_grant_type}, issuer={oauth_issuer}")
1214312148
LOGGER.info(f"DEBUG: Complete oauth_config = {oauth_config}")
1214412149

@@ -12411,6 +12416,11 @@ async def admin_edit_gateway(
1241112416
if scopes:
1241212417
oauth_config["scopes"] = scopes
1241312418

12419+
# Token endpoint auth method (RFC 6749 Section 2.3)
12420+
oauth_token_endpoint_auth_method = str(form.get("oauth_token_endpoint_auth_method", ""))
12421+
if oauth_token_endpoint_auth_method:
12422+
oauth_config["token_endpoint_auth_method"] = oauth_token_endpoint_auth_method
12423+
1241412424
LOGGER.info(f"✅ Assembled OAuth config from UI form fields (edit): grant_type={oauth_grant_type}, issuer={oauth_issuer}")
1241512425

1241612426
user_email = get_user_email(user)

mcpgateway/admin_ui/gateways.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,9 @@ export const editGateway = async function (gatewayId) {
400400
const oauthRedirectUriField = safeGetElement("oauth-redirect-uri-gw-edit");
401401
const oauthIssuerField = safeGetElement("oauth-issuer-gw-edit");
402402
const oauthScopesField = safeGetElement("oauth-scopes-gw-edit");
403+
const oauthTokenEndpointAuthMethodField = safeGetElement(
404+
"oauth-token-endpoint-auth-method-gw-edit"
405+
);
403406
const oauthAuthCodeFields = safeGetElement(
404407
"oauth-auth-code-fields-gw-edit"
405408
);
@@ -526,6 +529,13 @@ export const editGateway = async function (gatewayId) {
526529
? config.scopes.join(" ")
527530
: "";
528531
}
532+
if (
533+
oauthTokenEndpointAuthMethodField &&
534+
config.token_endpoint_auth_method
535+
) {
536+
oauthTokenEndpointAuthMethodField.value =
537+
config.token_endpoint_auth_method;
538+
}
529539
}
530540
break;
531541
case "query_param":

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

mcpgateway/templates/admin.html

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@
293293
></script>
294294
{% else %}
295295
<script
296-
src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.11/dist/cdn.min.js"
296+
src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.8/dist/cdn.min.js"
297297
integrity="{{ sri_hashes.alpinejs }}"
298298
crossorigin="anonymous"
299299
defer
@@ -322,7 +322,7 @@
322322
crossorigin="anonymous">
323323
</script>
324324
<script
325-
src="https://cdn.jsdelivr.net/npm/dompurify@3.4.0/dist/purify.min.js"
325+
src="https://cdn.jsdelivr.net/npm/dompurify@3.3.2/dist/purify.min.js"
326326
integrity="{{ sri_hashes.dompurify }}"
327327
crossorigin="anonymous">
328328
</script>
@@ -5965,6 +5965,25 @@ <h3 class="text-lg font-bold mb-4 dark:text-gray-200">
59655965
read:user")
59665966
</p>
59675967
</div>
5968+
5969+
<div>
5970+
<label
5971+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
5972+
>
5973+
Token Endpoint Auth Method
5974+
</label>
5975+
<select
5976+
name="oauth_token_endpoint_auth_method"
5977+
id="oauth-token-endpoint-auth-method-gw"
5978+
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"
5979+
>
5980+
<option value="client_secret_post">client_secret_post (credentials in POST body)</option>
5981+
<option value="client_secret_basic">client_secret_basic (HTTP Basic Auth header)</option>
5982+
</select>
5983+
<p class="mt-1 text-sm text-gray-500">
5984+
How client credentials are sent to the token endpoint (RFC 6749 Section 2.3)
5985+
</p>
5986+
</div>
59685987
</div>
59695988
</div>
59705989

@@ -10274,6 +10293,25 @@ <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
1027410293
read:user")
1027510294
</p>
1027610295
</div>
10296+
10297+
<div>
10298+
<label
10299+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
10300+
>
10301+
Token Endpoint Auth Method
10302+
</label>
10303+
<select
10304+
name="oauth_token_endpoint_auth_method"
10305+
id="oauth-token-endpoint-auth-method-gw-edit"
10306+
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"
10307+
>
10308+
<option value="client_secret_post">client_secret_post (credentials in POST body)</option>
10309+
<option value="client_secret_basic">client_secret_basic (HTTP Basic Auth header)</option>
10310+
</select>
10311+
<p class="mt-1 text-sm text-gray-500">
10312+
How client credentials are sent to the token endpoint (RFC 6749 Section 2.3)
10313+
</p>
10314+
</div>
1027710315
</div>
1027810316
</div>
1027910317
</div>

0 commit comments

Comments
 (0)