Skip to content

Commit 80c2cae

Browse files
author
Shamsul Arefin
committed
feat: Add OAuth 2.0 Authentication Support in Edit Gateway
Signed-off-by: Shamsul Arefin <[email protected]>
1 parent df0ad5d commit 80c2cae

File tree

5 files changed

+287
-11
lines changed

5 files changed

+287
-11
lines changed

mcpgateway/admin.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2914,6 +2914,20 @@ async def admin_edit_gateway(
29142914
else:
29152915
passthrough_headers = None
29162916

2917+
# Parse OAuth configuration if present
2918+
oauth_config_json = str(form.get("oauth_config"))
2919+
oauth_config: Optional[dict[str, Any]] = None
2920+
if oauth_config_json and oauth_config_json != "None":
2921+
try:
2922+
oauth_config = json.loads(oauth_config_json)
2923+
# Encrypt the client secret if present and not empty
2924+
if oauth_config and "client_secret" in oauth_config and oauth_config["client_secret"]:
2925+
encryption = get_oauth_encryption(settings.auth_encryption_secret)
2926+
oauth_config["client_secret"] = encryption.encrypt_secret(oauth_config["client_secret"])
2927+
except (json.JSONDecodeError, ValueError) as e:
2928+
LOGGER.error(f"Failed to parse OAuth config: {e}")
2929+
oauth_config = None
2930+
29172931
gateway = GatewayUpdate( # Pydantic validation happens here
29182932
name=str(form.get("name")),
29192933
url=str(form["url"]),
@@ -2929,6 +2943,7 @@ async def admin_edit_gateway(
29292943
auth_value=str(form.get("auth_value", "")),
29302944
auth_headers=auth_headers if auth_headers else None,
29312945
passthrough_headers=passthrough_headers,
2946+
oauth_config=oauth_config,
29322947
)
29332948
await gateway_service.update_gateway(db, gateway_id, gateway)
29342949
return JSONResponse(

mcpgateway/schemas.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2138,6 +2138,10 @@ class GatewayUpdate(BaseModelWithConfigDict):
21382138

21392139
# Adding `auth_value` as an alias for better access post-validation
21402140
auth_value: Optional[str] = Field(None, validate_default=True)
2141+
2142+
# OAuth 2.0 configuration
2143+
oauth_config: Optional[Dict[str, Any]] = Field(None, description="OAuth 2.0 configuration including grant_type, client_id, encrypted client_secret, URLs, and scopes")
2144+
21412145
tags: Optional[List[str]] = Field(None, description="Tags for categorizing the gateway")
21422146

21432147
@field_validator("tags")

mcpgateway/services/gateway_service.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,10 @@ async def update_gateway(self, db: Session, gateway_id: str, gateway_update: Gat
804804
gateway.auth_value = ""
805805

806806
# if auth_type is not None and only then check auth_value
807+
# Handle OAuth configuration updates
808+
if gateway_update.oauth_config is not None:
809+
gateway.oauth_config = gateway_update.oauth_config
810+
807811
if getattr(gateway, "auth_value", "") != "":
808812
token = gateway_update.auth_token
809813
password = gateway_update.auth_password

mcpgateway/static/admin.js

Lines changed: 149 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3183,6 +3183,7 @@ async function editGateway(gatewayId) {
31833183
const authHeadersSection = safeGetElement(
31843184
"auth-headers-fields-gw-edit",
31853185
);
3186+
const authOAuthSection = safeGetElement("auth-oauth-fields-gw-edit");
31863187

31873188
// Individual fields
31883189
const authUsernameField = safeGetElement(
@@ -3203,6 +3204,16 @@ async function editGateway(gatewayId) {
32033204
"auth-headers-fields-gw-edit",
32043205
)?.querySelector("input[name='auth_header_value']");
32053206

3207+
// OAuth fields
3208+
const oauthGrantTypeField = safeGetElement("oauth-grant-type-gw-edit");
3209+
const oauthClientIdField = safeGetElement("oauth-client-id-gw-edit");
3210+
const oauthClientSecretField = safeGetElement("oauth-client-secret-gw-edit");
3211+
const oauthTokenUrlField = safeGetElement("oauth-token-url-gw-edit");
3212+
const oauthAuthUrlField = safeGetElement("oauth-authorization-url-gw-edit");
3213+
const oauthRedirectUriField = safeGetElement("oauth-redirect-uri-gw-edit");
3214+
const oauthScopesField = safeGetElement("oauth-scopes-gw-edit");
3215+
const oauthAuthCodeFields = safeGetElement("oauth-auth-code-fields-gw-edit");
3216+
32063217
// Hide all auth sections first
32073218
if (authBasicSection) {
32083219
authBasicSection.style.display = "none";
@@ -3213,6 +3224,9 @@ async function editGateway(gatewayId) {
32133224
if (authHeadersSection) {
32143225
authHeadersSection.style.display = "none";
32153226
}
3227+
if (authOAuthSection) {
3228+
authOAuthSection.style.display = "none";
3229+
}
32163230

32173231
switch (gateway.authType) {
32183232
case "basic":
@@ -3245,6 +3259,41 @@ async function editGateway(gatewayId) {
32453259
}
32463260
}
32473261
break;
3262+
case "oauth":
3263+
if (authOAuthSection) {
3264+
authOAuthSection.style.display = "block";
3265+
}
3266+
// Populate OAuth fields if available
3267+
if (gateway.oauthConfig) {
3268+
const config = gateway.oauthConfig;
3269+
if (oauthGrantTypeField && config.grant_type) {
3270+
oauthGrantTypeField.value = config.grant_type;
3271+
// Show/hide authorization code fields based on grant type
3272+
if (oauthAuthCodeFields) {
3273+
oauthAuthCodeFields.style.display =
3274+
config.grant_type === "authorization_code" ? "block" : "none";
3275+
}
3276+
}
3277+
if (oauthClientIdField && config.client_id) {
3278+
oauthClientIdField.value = config.client_id;
3279+
}
3280+
if (oauthClientSecretField) {
3281+
oauthClientSecretField.value = ""; // Don't populate secret for security
3282+
}
3283+
if (oauthTokenUrlField && config.token_url) {
3284+
oauthTokenUrlField.value = config.token_url;
3285+
}
3286+
if (oauthAuthUrlField && config.authorization_url) {
3287+
oauthAuthUrlField.value = config.authorization_url;
3288+
}
3289+
if (oauthRedirectUriField && config.redirect_uri) {
3290+
oauthRedirectUriField.value = config.redirect_uri;
3291+
}
3292+
if (oauthScopesField && config.scopes && Array.isArray(config.scopes)) {
3293+
oauthScopesField.value = config.scopes.join(" ");
3294+
}
3295+
}
3296+
break;
32483297
case "":
32493298
default:
32503299
// No auth – keep everything hidden
@@ -3658,6 +3707,7 @@ function handleAuthTypeSelection(
36583707
basicFields,
36593708
bearerFields,
36603709
headersFields,
3710+
oauthFields,
36613711
) {
36623712
if (!basicFields || !bearerFields || !headersFields) {
36633713
console.warn("Auth field elements not found");
@@ -3666,30 +3716,40 @@ function handleAuthTypeSelection(
36663716

36673717
// Hide all fields first
36683718
[basicFields, bearerFields, headersFields].forEach((field) => {
3669-
field.style.display = "none";
3719+
if (field) field.style.display = "none";
36703720
});
36713721

3722+
// Hide OAuth fields if they exist
3723+
if (oauthFields) {
3724+
oauthFields.style.display = "none";
3725+
}
3726+
36723727
// Show relevant field based on selection
36733728
switch (value) {
36743729
case "basic":
3675-
basicFields.style.display = "block";
3730+
if (basicFields) basicFields.style.display = "block";
36763731
break;
36773732
case "bearer":
3678-
bearerFields.style.display = "block";
3733+
if (bearerFields) bearerFields.style.display = "block";
36793734
break;
36803735
case "authheaders": {
3681-
headersFields.style.display = "block";
3682-
// Ensure at least one header row is present
3683-
const containerId =
3684-
headersFields.querySelector('[id$="-container"]')?.id;
3685-
if (containerId) {
3686-
const container = document.getElementById(containerId);
3687-
if (container && container.children.length === 0) {
3688-
addAuthHeader(containerId);
3736+
if (headersFields) {
3737+
headersFields.style.display = "block";
3738+
// Ensure at least one header row is present
3739+
const containerId =
3740+
headersFields.querySelector('[id$="-container"]')?.id;
3741+
if (containerId) {
3742+
const container = document.getElementById(containerId);
3743+
if (container && container.children.length === 0) {
3744+
addAuthHeader(containerId);
3745+
}
36893746
}
36903747
}
36913748
break;
36923749
}
3750+
case "oauth":
3751+
if (oauthFields) oauthFields.style.display = "block";
3752+
break;
36933753
default:
36943754
// All fields already hidden
36953755
break;
@@ -6080,6 +6140,42 @@ async function handleEditGatewayFormSubmit(e) {
60806140
JSON.stringify(passthroughHeaders),
60816141
);
60826142

6143+
// Handle OAuth configuration
6144+
const authType = formData.get("auth_type");
6145+
if (authType === "oauth") {
6146+
const oauthConfig = {
6147+
grant_type: formData.get("oauth_grant_type"),
6148+
client_id: formData.get("oauth_client_id"),
6149+
client_secret: formData.get("oauth_client_secret"),
6150+
token_url: formData.get("oauth_token_url"),
6151+
scopes: formData.get("oauth_scopes")
6152+
? formData
6153+
.get("oauth_scopes")
6154+
.split(" ")
6155+
.filter((s) => s.trim())
6156+
: [],
6157+
};
6158+
6159+
// Add authorization code specific fields
6160+
if (oauthConfig.grant_type === "authorization_code") {
6161+
oauthConfig.authorization_url = formData.get(
6162+
"oauth_authorization_url",
6163+
);
6164+
oauthConfig.redirect_uri = formData.get("oauth_redirect_uri");
6165+
}
6166+
6167+
// Remove individual OAuth fields and add as oauth_config
6168+
formData.delete("oauth_grant_type");
6169+
formData.delete("oauth_client_id");
6170+
formData.delete("oauth_client_secret");
6171+
formData.delete("oauth_token_url");
6172+
formData.delete("oauth_scopes");
6173+
formData.delete("oauth_authorization_url");
6174+
formData.delete("oauth_redirect_uri");
6175+
6176+
formData.append("oauth_config", JSON.stringify(oauthConfig));
6177+
}
6178+
60836179
const isInactiveCheckedBool = isInactiveChecked("gateways");
60846180
formData.append("is_inactive_checked", isInactiveCheckedBool);
60856181
// Submit via fetch
@@ -6697,6 +6793,7 @@ function setupAuthenticationToggles() {
66976793
basicId: "auth-basic-fields-gw-edit",
66986794
bearerId: "auth-bearer-fields-gw-edit",
66996795
headersId: "auth-headers-fields-gw-edit",
6796+
oauthId: "auth-oauth-fields-gw-edit",
67006797
},
67016798
{
67026799
id: "edit-auth-type",
@@ -6765,6 +6862,15 @@ function setupFormHandlers() {
67656862
});
67666863
}
67676864

6865+
// Add OAuth grant type change handler for Edit Gateway modal
6866+
const editOAuthGrantTypeField = safeGetElement("oauth-grant-type-gw-edit");
6867+
if (editOAuthGrantTypeField) {
6868+
editOAuthGrantTypeField.addEventListener(
6869+
"change",
6870+
handleEditOAuthGrantTypeChange,
6871+
);
6872+
}
6873+
67686874
const toolForm = safeGetElement("add-tool-form");
67696875
if (toolForm) {
67706876
toolForm.addEventListener("submit", handleToolFormSubmit);
@@ -6907,6 +7013,38 @@ function handleOAuthGrantTypeChange() {
69077013
}
69087014
}
69097015

7016+
function handleEditOAuthGrantTypeChange() {
7017+
const grantType = this.value;
7018+
const authCodeFields = safeGetElement("oauth-auth-code-fields-gw-edit");
7019+
7020+
if (authCodeFields) {
7021+
if (grantType === "authorization_code") {
7022+
authCodeFields.style.display = "block";
7023+
7024+
// Make authorization code specific fields required
7025+
const requiredFields =
7026+
authCodeFields.querySelectorAll('input[type="url"]');
7027+
requiredFields.forEach((field) => {
7028+
field.required = true;
7029+
});
7030+
7031+
// Show additional validation for required fields
7032+
console.log(
7033+
"Authorization Code flow selected - additional fields are now required",
7034+
);
7035+
} else {
7036+
authCodeFields.style.display = "none";
7037+
7038+
// Remove required validation for hidden fields
7039+
const requiredFields =
7040+
authCodeFields.querySelectorAll('input[type="url"]');
7041+
requiredFields.forEach((field) => {
7042+
field.required = false;
7043+
});
7044+
}
7045+
}
7046+
}
7047+
69107048
function setupSchemaModeHandlers() {
69117049
const schemaModeRadios = document.getElementsByName("schema_input_mode");
69127050
const uiBuilderDiv = safeGetElement("ui-builder");

0 commit comments

Comments
 (0)