Skip to main content
Glama
Azure-Samples

Secure Remote MCP Server

consent.policy.xml84.4 kB
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <!-- Consent Policy - Handles user consent for OAuth client applications --> <policies> <inbound> <base /> <!-- Extract form body once --> <set-variable name="form_body" value="@{ if (context.Request.Method == "POST") { string contentType = context.Request.Headers.GetValueOrDefault("Content-Type", ""); if (contentType.Contains("application/x-www-form-urlencoded")) { return context.Request.Body.As<string>(preserveContent: true); } } return ""; }" /> <!-- Extract individual parameters with consistent decoding --> <set-variable name="client_id" value="@{ string formBody = context.Variables.GetValueOrDefault<string>("form_body", ""); // Check form data first (POST) if (!string.IsNullOrEmpty(formBody)) { string[] pairs = formBody.Split('&'); foreach (string pair in pairs) { string[] keyValue = pair.Split(new char[] {'='}, 2); if (keyValue.Length == 2 && keyValue[0] == "client_id") { return System.Net.WebUtility.UrlDecode(keyValue[1]); } } } // Fallback to query string (GET) string queryValue = (string)context.Request.Url.Query.GetValueOrDefault("client_id", ""); return !string.IsNullOrEmpty(queryValue) ? System.Net.WebUtility.UrlDecode(queryValue) : ""; }" /> <set-variable name="redirect_uri" value="@{ string formBody = context.Variables.GetValueOrDefault<string>("form_body", ""); // Check form data first (POST) if (!string.IsNullOrEmpty(formBody)) { string[] pairs = formBody.Split('&'); foreach (string pair in pairs) { string[] keyValue = pair.Split(new char[] {'='}, 2); if (keyValue.Length == 2 && keyValue[0] == "redirect_uri") { return keyValue[1]; } } } // Fallback to query string (GET) return (string)context.Request.Url.Query.GetValueOrDefault("redirect_uri", ""); }" /> <set-variable name="state" value="@{ string formBody = context.Variables.GetValueOrDefault<string>("form_body", ""); // Check form data first (POST) if (!string.IsNullOrEmpty(formBody)) { string[] pairs = formBody.Split('&'); foreach (string pair in pairs) { string[] keyValue = pair.Split(new char[] {'='}, 2); if (keyValue.Length == 2 && keyValue[0] == "state") { return System.Net.WebUtility.UrlDecode(keyValue[1]); } } } // Fallback to query string (GET) string queryValue = (string)context.Request.Url.Query.GetValueOrDefault("state", ""); return !string.IsNullOrEmpty(queryValue) ? System.Net.WebUtility.UrlDecode(queryValue) : ""; }" /> <set-variable name="code_challenge" value="@{ string formBody = context.Variables.GetValueOrDefault<string>("form_body", ""); // Check form data first (POST) if (!string.IsNullOrEmpty(formBody)) { string[] pairs = formBody.Split('&'); foreach (string pair in pairs) { string[] keyValue = pair.Split(new char[] {'='}, 2); if (keyValue.Length == 2 && keyValue[0] == "code_challenge") { return keyValue[1]; } } } // Fallback to query string (GET) return (string)context.Request.Url.Query.GetValueOrDefault("code_challenge", ""); }" /> <set-variable name="code_challenge_method" value="@{ string formBody = context.Variables.GetValueOrDefault<string>("form_body", ""); // Check form data first (POST) if (!string.IsNullOrEmpty(formBody)) { string[] pairs = formBody.Split('&'); foreach (string pair in pairs) { string[] keyValue = pair.Split(new char[] {'='}, 2); if (keyValue.Length == 2 && keyValue[0] == "code_challenge_method") { return keyValue[1]; } } } // Fallback to query string (GET) return (string)context.Request.Url.Query.GetValueOrDefault("code_challenge_method", ""); }" /> <set-variable name="access_denied_template" value="@{ return @"<html lang='en'> <head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'> <title>Access Denied</title> <style> __COMMON_STYLES__ .error-details { background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; margin: 20px 0; font-family: 'Courier New', Consolas, monospace; font-size: 14px; line-height: 1.6; white-space: pre-wrap; overflow-x: auto; } .error-title { color: #dc3545; font-weight: bold; margin-bottom: 10px; } .debug-section { margin-top: 15px; padding-top: 15px; border-top: 1px solid #dee2e6; } .debug-label { font-weight: bold; color: #495057; } </style> </head> <body> <div class='consent-container'> <h1 class='denial-heading'>Access Denied</h1> <div class='error-details'> <div class='error-title'>Error Details:</div> __DENIAL_MESSAGE__ </div> <p>The application will not be able to access your data.</p> <p>You can close this window safely.</p> </div> </body> </html>"; }" /> <!-- Reusable function to generate 403 error response --> <set-variable name="generate_403_response" value="@{ string errorTemplate = context.Variables.GetValueOrDefault<string>("access_denied_template"); string commonStyles = context.Variables.GetValueOrDefault<string>("common_styles"); string message = "Access denied."; // Replace placeholders with actual content errorTemplate = errorTemplate.Replace("__COMMON_STYLES__", commonStyles); errorTemplate = errorTemplate.Replace("__DENIAL_MESSAGE__", message); return errorTemplate; }" /> <!-- Error page template --> <set-variable name="client_not_found_template" value="@{ return @"<html lang='en'> <head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'> <title>Client Not Found</title> <style> __COMMON_STYLES__ </style> </head> <body> <div class='consent-container'> <h1 class='denial-heading'>Client Not Found</h1> <p>The client registration for the specified client was not found.</p> <div class='client-info'> <p><strong>Client ID:</strong> <code>__CLIENT_ID_DISPLAY__</code></p> <p><strong>Redirect URI:</strong> <code>__REDIRECT_URI__</code></p> </div> <p>Please ensure that you are using a properly registered client application.</p> <p>You can close this window safely.</p> </div> </body> </html>"; }" /> <!-- Normalize redirect URI by handling potential double-encoding --> <set-variable name="normalized_redirect_uri" value="@{ string redirectUri = context.Variables.GetValueOrDefault<string>("redirect_uri", ""); if (string.IsNullOrEmpty(redirectUri)) { return ""; } try { string firstDecode = System.Net.WebUtility.UrlDecode(redirectUri); // Check if still encoded (contains % followed by hex digits) if (firstDecode.Contains("%") && System.Text.RegularExpressions.Regex.IsMatch(firstDecode, @"%[0-9A-Fa-f]{2}")) { // Double-encoded, decode again string secondDecode = System.Net.WebUtility.UrlDecode(firstDecode); return secondDecode; } else { // Single encoding, first decode is sufficient return firstDecode; } } catch (Exception) { // If decoding fails, return original value return redirectUri; } }" /> <!-- Cache client information lookup --> <cache-lookup-value key="@($"ClientInfo-{context.Variables.GetValueOrDefault<string>("client_id")}")" variable-name="clientInfoJson" /> <!-- If cache lookup failed, try to retrieve from CosmosDB --> <choose> <when condition="@(string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("clientInfoJson")))"> <!-- Get CosmosDB access token using managed identity --> <authentication-managed-identity resource="https://cosmos.azure.com" output-token-variable-name="cosmosAccessToken" /> <send-request mode="new" response-variable-name="cosmosClientResponse" timeout="30" ignore-error="true"> <set-url>@($"{{CosmosDbEndpoint}}/dbs/{{CosmosDbDatabase}}/colls/{{CosmosDbContainer}}/docs/{context.Variables.GetValueOrDefault<string>("client_id")}")</set-url> <set-method>GET</set-method> <set-header name="Content-Type" exists-action="override"> <value>application/json</value> </set-header> <set-header name="x-ms-version" exists-action="override"> <value>2018-12-31</value> </set-header> <set-header name="x-ms-partitionkey" exists-action="override"> <value>@($"[\"{context.Variables.GetValueOrDefault<string>("client_id")}\"]")</value> </set-header> <set-header name="Authorization" exists-action="override"> <value>@($"type=aad&ver=1.0&sig={context.Variables.GetValueOrDefault<string>("cosmosAccessToken")}")</value> </set-header> </send-request> <!-- If CosmosDB request was successful, extract client info --> <choose> <when condition="@(((IResponse)context.Variables["cosmosClientResponse"]).StatusCode == 200)"> <set-variable name="clientInfoJson" value="@{ var cosmosResponse = (IResponse)context.Variables["cosmosClientResponse"]; var cosmosDocument = cosmosResponse.Body.As<JObject>(); // Extract the client info fields we need var clientInfo = new JObject(); clientInfo["client_name"] = cosmosDocument["client_name"]; clientInfo["client_uri"] = cosmosDocument["client_uri"]; clientInfo["redirect_uris"] = cosmosDocument["redirect_uris"]; return clientInfo.ToString(); }" /> <!-- Store in cache for future requests --> <cache-store-value duration="3600" key="@($"ClientInfo-{context.Variables.GetValueOrDefault<string>("client_id")}")" value="@(context.Variables.GetValueOrDefault<string>("clientInfoJson"))" /> </when> </choose> </when> </choose> <!-- Get OAuth scopes from configuration --> <set-variable name="oauth_scopes" value="{{OAuthScopes}}" /> <!-- Generate CSRF token for form protection (GET requests only) --> <set-variable name="csrf_token" value="@{ // Only generate tokens for GET requests (showing consent form) // POST requests validate existing tokens, not generate new ones if (context.Request.Method != "GET") { return ""; } // Generate random CSRF token using Guid and timestamp string guidPart = Guid.NewGuid().ToString("N"); string timestampPart = DateTime.UtcNow.Ticks.ToString(); string combinedString = guidPart + timestampPart; // Create URL-safe token by encoding combined string string token = System.Convert.ToBase64String( System.Text.Encoding.UTF8.GetBytes(combinedString) ).Replace("+", "-").Replace("/", "_").Replace("=", "").Substring(0, 32); return token; }" /> <!-- Cache CSRF token for validation (GET requests only) --> <choose> <when condition="@(context.Request.Method == "GET" && !string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("csrf_token")))"> <cache-store-value key="@($"CSRF-{context.Variables.GetValueOrDefault<string>("csrf_token")}")" value="@{ string clientId = context.Variables.GetValueOrDefault<string>("client_id", ""); string normalizedRedirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); string timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); string tokenData = $"{clientId}:{normalizedRedirectUri}:{timestamp}"; // Add debugging metadata string debugInfo = $"CACHED_AT:{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ}"; return $"{tokenData}|{debugInfo}"; }" duration="900" /> <!-- Track token caching for debugging --> <set-variable name="csrf_token_cached" value="true" /> </when> <otherwise> <set-variable name="csrf_token_cached" value="false" /> </otherwise> </choose> <!-- Validate client registration --> <set-variable name="is_client_registered" value="@{ try { string clientId = context.Variables.GetValueOrDefault<string>("client_id", ""); string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); if (string.IsNullOrEmpty(clientId)) { return false; } // Get client info from cache lookup string clientInfoJson = context.Variables.GetValueOrDefault<string>("clientInfoJson"); if (string.IsNullOrEmpty(clientInfoJson)) { return false; } // Parse client configuration JObject clientInfo = JObject.Parse(clientInfoJson); JArray redirectUris = clientInfo["redirect_uris"]?.ToObject<JArray>(); // Validate redirect URI is registered if (redirectUris != null) { foreach (var uri in redirectUris) { // Normalize registered URI for comparison string registeredUri = System.Net.WebUtility.UrlDecode(uri.ToString()); if (registeredUri == redirectUri) { return true; } } } return false; } catch (Exception ex) { return false; } }" /> <!-- Extract client name from cache --> <set-variable name="client_name" value="@{ try { string clientId = context.Variables.GetValueOrDefault<string>("client_id", ""); if (string.IsNullOrEmpty(clientId)) { return "Unknown Application"; } // Get client info from cache lookup string clientInfoJson = context.Variables.GetValueOrDefault<string>("clientInfoJson"); if (string.IsNullOrEmpty(clientInfoJson)) { return clientId; } // Parse client configuration JObject clientInfo = JObject.Parse(clientInfoJson); string clientName = clientInfo["client_name"]?.ToString(); return string.IsNullOrEmpty(clientName) ? clientId : clientName; } catch (Exception ex) { return context.Variables.GetValueOrDefault<string>("client_id", "Unknown Application"); } }" /> <!-- Extract client URI from cache --> <set-variable name="client_uri" value="@{ try { // Get client info from cache lookup string clientInfoJson = context.Variables.GetValueOrDefault<string>("clientInfoJson"); if (string.IsNullOrEmpty(clientInfoJson)) { return "N/A"; } // Parse client configuration JObject clientInfo = JObject.Parse(clientInfoJson); string clientUri = clientInfo["client_uri"]?.ToString(); return string.IsNullOrEmpty(clientUri) ? "N/A" : clientUri; } catch (Exception ex) { return "N/A"; } }" /> <!-- Define common styles for consent and error pages --> <set-variable name="common_styles" value="@{ return @" body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 100%; margin: 0; padding: 0; line-height: 1.6; min-height: 100vh; background: linear-gradient(135deg, #1f1f1f, #333344, #3f4066); /* Modern dark gradient */ color: #333333; display: flex; justify-content: center; align-items: center; }.container, .consent-container { background-color: #ffffff; border-radius: 4px; /* Adding some subtle rounding */ padding: 30px; max-width: 600px; width: 90%; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); border: none; } h1 { margin-bottom: 20px; border-bottom: 1px solid #EDEBE9; padding-bottom: 10px; font-weight: 500; } .consent-heading { color: #0078D4; /* Microsoft Blue */ } .denial-heading { color: #D83B01; /* Microsoft Attention color */ } p { margin: 15px 0; line-height: 1.7; color: #323130; /* Microsoft text color */ } .client-info { background-color: #F5F5F5; /* Light gray background for info boxes */ padding: 15px; border-radius: 4px; /* Adding some subtle rounding */ margin: 15px 0; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); border: 1px solid #EDEBE9; } .client-info p { display: flex; align-items: flex-start; margin: 8px 0; } .client-info strong { min-width: 160px; flex-shrink: 0; text-align: left; padding-right: 15px; color: #0078D4; /* Microsoft Blue */ } .client-info code { font-family: 'Consolas', 'Monaco', 'Courier New', monospace; background-color: rgba(240, 240, 250, 0.5); padding: 2px 6px; border-radius: 4px; /* Adding some subtle rounding */ color: #0078D4; /* Microsoft Blue */ word-break: break-all; } .btn { display: inline-block; padding: 8px 16px; margin: 10px 0; border-radius: 4px; /* Adding some subtle rounding */ text-decoration: none; font-weight: 600; cursor: pointer; transition: all 0.2s ease; } .btn-primary { background-color: #0078D4; /* Microsoft Blue */ color: white; border: none; } .btn-primary:hover { background-color: #106EBE; /* Microsoft Blue hover */ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .btn-secondary { background-color: #D83B01; /* Microsoft Red */ color: white; /* White text */ border: none; } .btn-secondary:hover { background-color: #A80000; /* Darker red on hover */ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .buttons { margin-top: 20px; display: flex; gap: 10px; justify-content: flex-start; } a { color: #0078D4; /* Microsoft Blue */ text-decoration: none; font-weight: 600; } a:hover { text-decoration: underline; } strong { color: #0078D4; /* Microsoft Blue */ font-weight: 600; } .error-message { background-color: #FDE7E9; /* Light red background */ padding: 15px; margin: 15px 0; border-radius: 4px; /* Adding some subtle rounding */ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); border-left: 3px solid #D83B01; /* Microsoft Attention color */ } .error-message p { margin: 8px 0; } .error-message p:first-child { font-weight: 500; color: #D83B01; /* Microsoft Attention color */ }"; }" /> <!-- Consent page HTML template --> <set-variable name="consent_page_template" value="@{ return @"<html lang='en'> <head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'> <title>Application Consent</title> <style> __COMMON_STYLES__ /* Additional styles for scopes list */ .scopes-list { margin: 0; padding-left: 0; } .scopes-list li { list-style-type: none; padding: 4px 0; display: flex; } </style> </head> <body> <div class='consent-container'> <h1 class='consent-heading'>Application Access Request</h1> <p>The following application is requesting access to <strong>{{MCPServerName}}</strong>, which might include access to everything <strong>{{MCPServerName}}</strong> has been and will be granted access to.</p> <div class='client-info'> <p><strong>Application Name:</strong> <code>__CLIENT_NAME__</code></p> <p><strong>Application Website:</strong> <code>__CLIENT_URI__</code></p> <p><strong>Application ID:</strong> <code>__CLIENT_ID_DISPLAY__</code></p> <p><strong>Redirect URI:</strong> <code>__REDIRECT_URI__</code></p> </div> <p>The application will have access to the following scopes, used by <strong>{{MCPServerName}}</strong>:</p> <div class='client-info'> <ul class='scopes-list'> <li>__OAUTH_SCOPES__</li> </ul> </div> <div class='buttons'> <form method='post' action='__CONSENT_ACTION_URL__' style='display: inline-block;'> <input type='hidden' name='client_id' value='__CLIENT_ID_FORM__'> <input type='hidden' name='redirect_uri' value='__REDIRECT_URI__'> <input type='hidden' name='state' value='__STATE__'> <input type='hidden' name='code_challenge' value='__CODE_CHALLENGE__'> <input type='hidden' name='code_challenge_method' value='__CODE_CHALLENGE_METHOD__'> <input type='hidden' name='csrf_token' value='__CSRF_TOKEN__'> <input type='hidden' name='consent_action' value='allow'> <button type='submit' class='btn btn-primary'>Allow</button> </form> <form method='post' action='__CONSENT_ACTION_URL__' style='display: inline-block;'> <input type='hidden' name='client_id' value='__CLIENT_ID_FORM__'> <input type='hidden' name='redirect_uri' value='__REDIRECT_URI__'> <input type='hidden' name='state' value='__STATE__'> <input type='hidden' name='code_challenge' value='__CODE_CHALLENGE__'> <input type='hidden' name='code_challenge_method' value='__CODE_CHALLENGE_METHOD__'> <input type='hidden' name='csrf_token' value='__CSRF_TOKEN__'> <input type='hidden' name='consent_action' value='deny'> <button type='submit' class='btn btn-secondary'>Deny</button> </form> </div> </div> </body> </html>"; }" /> <!-- Check for existing client denial cookie --> <set-variable name="has_denial_cookie" value="@{ try { string clientId = context.Variables.GetValueOrDefault<string>("client_id", ""); string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(redirectUri)) { return false; } var cookieHeader = context.Request.Headers.GetValueOrDefault("Cookie", ""); if (string.IsNullOrEmpty(cookieHeader)) { return false; } string cookieName = "__Host-MCP_DENIED_CLIENTS"; string[] cookies = cookieHeader.Split(';'); foreach (string cookie in cookies) { string trimmedCookie = cookie.Trim(); if (trimmedCookie.StartsWith(cookieName + "=")) { string cookieValue = trimmedCookie.Substring(cookieName.Length + 1); try { string decodedValue = System.Text.Encoding.UTF8.GetString( System.Convert.FromBase64String(cookieValue.Split('.')[0])); JArray clients = JArray.Parse(decodedValue); string clientKey = $"{clientId}:{redirectUri}"; foreach (var item in clients) { string itemString = item.ToString(); if (itemString == clientKey) { return true; } // Handle URL-encoded redirect URI in stored cookie try { if (itemString.Contains(':')) { string[] parts = itemString.Split(new char[] {':'}, 2); if (parts.Length == 2) { string storedClientId = parts[0]; string storedRedirectUri = System.Net.WebUtility.UrlDecode(parts[1]); if (storedClientId == clientId && storedRedirectUri == redirectUri) { return true; } } } } catch (Exception ex) { // Ignore comparison errors and continue } } } catch (Exception ex) { // Ignore cookie parsing errors and continue } } } return false; } catch (Exception ex) { return false; } }" /> <!-- Check for existing client approval cookie --> <set-variable name="has_approval_cookie" value="@{ try { string clientId = context.Variables.GetValueOrDefault<string>("client_id", ""); string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(redirectUri)) { return false; } var cookieHeader = context.Request.Headers.GetValueOrDefault("Cookie", ""); if (string.IsNullOrEmpty(cookieHeader)) { return false; } string cookieName = "__Host-MCP_APPROVED_CLIENTS"; string[] cookies = cookieHeader.Split(';'); foreach (string cookie in cookies) { string trimmedCookie = cookie.Trim(); if (trimmedCookie.StartsWith(cookieName + "=")) { string cookieValue = trimmedCookie.Substring(cookieName.Length + 1); try { string decodedValue = System.Text.Encoding.UTF8.GetString( System.Convert.FromBase64String(cookieValue.Split('.')[0])); JArray clients = JArray.Parse(decodedValue); string clientKey = $"{clientId}:{redirectUri}"; foreach (var item in clients) { string itemString = item.ToString(); if (itemString == clientKey) { return true; } // Handle URL-encoded redirect URI in stored cookie try { if (itemString.Contains(':')) { string[] parts = itemString.Split(new char[] {':'}, 2); if (parts.Length == 2) { string storedClientId = parts[0]; string storedRedirectUri = System.Net.WebUtility.UrlDecode(parts[1]); if (storedClientId == clientId && storedRedirectUri == redirectUri) { return true; } } } } catch (Exception ex) { // Ignore comparison errors and continue } } } catch (Exception ex) { // Ignore cookie parsing errors and continue } } } return false; } catch (Exception ex) { return false; } }" /> <set-variable name="consent_action" value="@{ string formBody = context.Variables.GetValueOrDefault<string>("form_body", ""); // Check form data first (POST) if (!string.IsNullOrEmpty(formBody)) { string[] pairs = formBody.Split('&'); foreach (string pair in pairs) { string[] keyValue = pair.Split(new char[] {'='}, 2); if (keyValue.Length == 2 && keyValue[0] == "consent_action") { return System.Net.WebUtility.UrlDecode(keyValue[1]); } } } // Fallback to query string (GET) string queryValue = (string)context.Request.Url.Query.GetValueOrDefault("consent_action", ""); return !string.IsNullOrEmpty(queryValue) ? System.Net.WebUtility.UrlDecode(queryValue) : ""; }" /> <!-- Extract CSRF token from form data --> <set-variable name="csrf_token_from_form" value="@{ string formBody = context.Variables.GetValueOrDefault<string>("form_body", ""); // Check form data first (POST) if (!string.IsNullOrEmpty(formBody)) { string[] pairs = formBody.Split('&'); foreach (string pair in pairs) { string[] keyValue = pair.Split(new char[] {'='}, 2); if (keyValue.Length == 2 && keyValue[0] == "csrf_token") { return System.Net.WebUtility.UrlDecode(keyValue[1]); } } } // Fallback to query string (GET) string queryValue = (string)context.Request.Url.Query.GetValueOrDefault("csrf_token", ""); return !string.IsNullOrEmpty(queryValue) ? System.Net.WebUtility.UrlDecode(queryValue) : ""; }" /> <!-- Validate CSRF token for POST requests --> <set-variable name="csrf_valid" value="@{ if (context.Request.Method != "POST") { return true; // Only validate POST requests } string submittedToken = context.Variables.GetValueOrDefault<string>("csrf_token_from_form", ""); if (string.IsNullOrEmpty(submittedToken)) { return false; } // Token cache lookup validation happens next string cacheKey = $"CSRF-{submittedToken}"; return true; // Initial validation passes, detailed validation follows }" /> <!-- Validate Origin/Referer headers for CSRF protection --> <set-variable name="origin_referer_valid" value="@{ if (context.Request.Method != "POST") { return true; // Only validate state-changing operations } // Get the target origin (expected origin) string targetOrigin = "{{APIMGatewayURL}}"; // Remove protocol and trailing slash for comparison if (targetOrigin.StartsWith("https://")) { targetOrigin = targetOrigin.Substring(8); } else if (targetOrigin.StartsWith("http://")) { targetOrigin = targetOrigin.Substring(7); } if (targetOrigin.EndsWith("/")) { targetOrigin = targetOrigin.TrimEnd('/'); } // First check Origin header (preferred) string originHeader = context.Request.Headers.GetValueOrDefault("Origin", ""); if (!string.IsNullOrEmpty(originHeader)) { try { Uri originUri = new Uri(originHeader); string sourceOrigin = originUri.Host; if (originUri.Port != 80 && originUri.Port != 443) { sourceOrigin += ":" + originUri.Port; } if (sourceOrigin.Equals(targetOrigin, StringComparison.OrdinalIgnoreCase)) { return true; } else { return false; } } catch (Exception ex) { return false; } } // Fallback to Referer header if Origin is not present string refererHeader = context.Request.Headers.GetValueOrDefault("Referer", ""); if (!string.IsNullOrEmpty(refererHeader)) { try { Uri refererUri = new Uri(refererHeader); string sourceOrigin = refererUri.Host; if (refererUri.Port != 80 && refererUri.Port != 443) { sourceOrigin += ":" + refererUri.Port; } if (sourceOrigin.Equals(targetOrigin, StringComparison.OrdinalIgnoreCase)) { return true; } else { return false; } } catch (Exception ex) { return false; } } // Neither Origin nor Referer header present - this is suspicious for POST requests // OWASP recommends blocking such requests for better security return false; // Block requests without proper origin validation }" /> <!-- Validate Fetch Metadata headers for CSRF protection --> <set-variable name="fetch_metadata_valid" value="@{ // Check Sec-Fetch-Site header for cross-site request detection string secFetchSite = context.Request.Headers.GetValueOrDefault("Sec-Fetch-Site", ""); // Allow same-origin, same-site, and direct navigation if (string.IsNullOrEmpty(secFetchSite) || secFetchSite == "same-origin" || secFetchSite == "same-site" || secFetchSite == "none") { return true; } // Block cross-site POST requests if (context.Request.Method == "POST" && secFetchSite == "cross-site") { return false; } // Allow other values for compatibility return true; }" /> <!-- Lookup CSRF token from cache --> <cache-lookup-value key="@($"CSRF-{context.Variables.GetValueOrDefault<string>("csrf_token_from_form")}")" variable-name="csrf_token_data" /> <!-- Validate CSRF token details --> <set-variable name="csrf_validation_result" value="@{ if (context.Request.Method != "POST") { return "valid"; // No validation needed for GET requests } string submittedToken = context.Variables.GetValueOrDefault<string>("csrf_token_from_form", ""); if (string.IsNullOrEmpty(submittedToken)) { return "missing_token"; } string tokenData = context.Variables.GetValueOrDefault<string>("csrf_token_data"); if (string.IsNullOrEmpty(tokenData)) { return "invalid_token"; } try { // Extract token data (before debug info separator) string actualTokenData = tokenData; if (tokenData.Contains("|")) { actualTokenData = tokenData.Split('|')[0]; } // Parse token data: client_id:redirect_uri:timestamp // Since both redirect_uri and timestamp can contain colons, we need to be very careful // The timestamp format is: YYYY-MM-DDTHH:mm:ssZ // So we look for the last occurrence of a timestamp pattern // Find the last occurrence of a timestamp pattern (YYYY-MM-DDTHH:mm:ssZ) var timestampPattern = @":\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$"; var timestampMatch = System.Text.RegularExpressions.Regex.Match(actualTokenData, timestampPattern); if (!timestampMatch.Success) { return "malformed_token"; } // Extract the timestamp (without the leading colon) string timestampStr = timestampMatch.Value.Substring(1); // Extract everything before the timestamp match as the client_id:redirect_uri part string clientAndRedirect = actualTokenData.Substring(0, timestampMatch.Index); // Split client_id:redirect_uri on the first colon only int firstColonIndex = clientAndRedirect.IndexOf(':'); if (firstColonIndex == -1) { return "malformed_token"; } string tokenClientId = clientAndRedirect.Substring(0, firstColonIndex); string tokenRedirectUri = clientAndRedirect.Substring(firstColonIndex + 1); // Validate client_id and redirect_uri match using constant-time comparison string currentClientId = context.Variables.GetValueOrDefault<string>("client_id", ""); string currentRedirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); // Constant-time string comparison for client_id to prevent timing attacks bool clientIdMatches = true; if (tokenClientId == null || currentClientId == null) { clientIdMatches = (tokenClientId == currentClientId); } else if (tokenClientId.Length != currentClientId.Length) { clientIdMatches = false; } else { int result = 0; for (int i = 0; i < tokenClientId.Length; i++) { result |= tokenClientId[i] ^ currentClientId[i]; } clientIdMatches = (result == 0); } if (!clientIdMatches) { return "client_mismatch"; } // Constant-time string comparison for redirect_uri to prevent timing attacks bool redirectUriMatches = true; if (tokenRedirectUri == null || currentRedirectUri == null) { redirectUriMatches = (tokenRedirectUri == currentRedirectUri); } else if (tokenRedirectUri.Length != currentRedirectUri.Length) { redirectUriMatches = false; } else { int result = 0; for (int i = 0; i < tokenRedirectUri.Length; i++) { result |= tokenRedirectUri[i] ^ currentRedirectUri[i]; } redirectUriMatches = (result == 0); } if (!redirectUriMatches) { return "redirect_mismatch"; } // Validate timestamp (token should not be older than 15 minutes) DateTime tokenTime; try { tokenTime = DateTime.Parse(timestampStr); } catch (Exception) { return "invalid_timestamp"; } TimeSpan age = DateTime.UtcNow - tokenTime; if (age.TotalMinutes > 15) { return "expired_token"; } return "valid"; } catch (Exception ex) { return "validation_error"; } }" /> <!-- If this is a form submission, process the consent choice --> <choose> <when condition="@(context.Request.Method == "POST")"> <!-- Validate Origin/Referer headers --> <choose> <when condition="@(!context.Variables.GetValueOrDefault<bool>("origin_referer_valid"))"> <!-- Origin/Referer validation failed --> <return-response> <set-status code="403" reason="Forbidden" /> <set-header name="Content-Type" exists-action="override"> <value>text/html</value> </set-header> <set-header name="Cache-Control" exists-action="override"> <value>no-store, no-cache</value> </set-header> <set-header name="Pragma" exists-action="override"> <value>no-cache</value> </set-header> <set-body>@(context.Variables.GetValueOrDefault<string>("generate_403_response"))</set-body> </return-response> </when> <otherwise> <!-- Origin/Referer validation passed --> <!-- Validate Fetch Metadata headers --> <choose> <when condition="@(!context.Variables.GetValueOrDefault<bool>("fetch_metadata_valid"))"> <!-- Fetch metadata validation failed --> <return-response> <set-status code="403" reason="Forbidden" /> <set-header name="Content-Type" exists-action="override"> <value>text/html</value> </set-header> <set-header name="Cache-Control" exists-action="override"> <value>no-store, no-cache</value> </set-header> <set-header name="Pragma" exists-action="override"> <value>no-cache</value> </set-header> <set-body>@(context.Variables.GetValueOrDefault<string>("generate_403_response"))</set-body> </return-response> </when> <otherwise> <!-- Fetch metadata validation passed --> <!-- Validate CSRF token --> <choose> <when condition="@(context.Variables.GetValueOrDefault<string>("csrf_validation_result") != "valid")"> <!-- CSRF validation failed --> <return-response> <set-status code="403" reason="Forbidden" /> <set-header name="Content-Type" exists-action="override"> <value>text/html</value> </set-header> <set-header name="Cache-Control" exists-action="override"> <value>no-store, no-cache</value> </set-header> <set-header name="Pragma" exists-action="override"> <value>no-cache</value> </set-header> <set-body>@(context.Variables.GetValueOrDefault<string>("generate_403_response"))</set-body> </return-response> </when> <otherwise> <!-- CSRF validation passed --> <!-- Delete CSRF token from cache to prevent reuse --> <cache-remove-value key="@($"CSRF-{context.Variables.GetValueOrDefault<string>("csrf_token_from_form")}")" /> <choose> <when condition="@(context.Variables.GetValueOrDefault<string>("consent_action") == "allow")"> <!-- Process consent approval --> <set-variable name="response_status_code" value="302" /> <set-variable name="response_redirect_location" value="@{ string baseUrl = "{{APIMGatewayURL}}"; string clientId = context.Variables.GetValueOrDefault<string>("client_id", ""); string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); string originalState = context.Variables.GetValueOrDefault<string>("state", ""); string encodedClientId = System.Net.WebUtility.UrlEncode(clientId); string encodedRedirectUri = System.Net.WebUtility.UrlEncode(redirectUri); // State should be used as-is since it's already properly formatted from the original request string encodedState = originalState; // Add PKCE parameters if they exist string codeChallenge = context.Variables.GetValueOrDefault<string>("code_challenge", ""); string codeChallengeMethod = context.Variables.GetValueOrDefault<string>("code_challenge_method", ""); string url = $"{baseUrl}/authorize?client_id={encodedClientId}&redirect_uri={encodedRedirectUri}&state={encodedState}"; if (!string.IsNullOrEmpty(codeChallenge)) { url += $"&code_challenge={System.Net.WebUtility.UrlEncode(codeChallenge)}"; } if (!string.IsNullOrEmpty(codeChallengeMethod)) { url += $"&code_challenge_method={System.Net.WebUtility.UrlEncode(codeChallengeMethod)}"; } return url; }" /> <!-- Calculate approval cookie value --> <set-variable name="approval_cookie" value="@{ string cookieName = "__Host-MCP_APPROVED_CLIENTS"; // Use already extracted parameters instead of re-parsing form data string clientId = context.Variables.GetValueOrDefault<string>("client_id", ""); string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); // Create a unique identifier for this client/redirect combination string clientKey = $"{clientId}:{redirectUri}"; // Check for existing cookie var cookieHeader = context.Request.Headers.GetValueOrDefault("Cookie", ""); JArray approvedClients = new JArray(); if (!string.IsNullOrEmpty(cookieHeader)) { // Parse cookies to find our approval cookie string[] cookies = cookieHeader.Split(';'); foreach (string cookie in cookies) { string trimmedCookie = cookie.Trim(); if (trimmedCookie.StartsWith(cookieName + "=")) { try { // Extract and parse the cookie value string cookieValue = trimmedCookie.Substring(cookieName.Length + 1); // Get the payload part (before the first dot if cookie is signed) string payload = cookieValue.Contains('.') ? cookieValue.Split('.')[0] : cookieValue; string decodedValue = System.Text.Encoding.UTF8.GetString( System.Convert.FromBase64String(payload)); approvedClients = JArray.Parse(decodedValue); } catch (Exception) { // If parsing fails, we'll just create a new cookie approvedClients = new JArray(); } break; } } } // Add the current client if not already in the list bool clientExists = false; foreach (var item in approvedClients) { if (item.ToString() == clientKey) { clientExists = true; break; } } if (!clientExists) { approvedClients.Add(clientKey); } // Base64 encode the client list string jsonClients = approvedClients.ToString(Newtonsoft.Json.Formatting.None); string encodedClients = System.Convert.ToBase64String( System.Text.Encoding.UTF8.GetBytes(jsonClients)); // Return the full cookie string with appropriate settings return $"{cookieName}={encodedClients}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax"; }" /> <!-- Set variables for outbound policy awareness --> <set-variable name="consent_approved" value="true" /> <set-variable name="cookie_name" value="__Host-MCP_APPROVED_CLIENTS" /> <!-- Return the response with the cookie already set --> <return-response> <set-status code="302" reason="Found" /> <set-header name="Location" exists-action="override"> <value>@(context.Variables.GetValueOrDefault<string>("response_redirect_location", ""))</value> </set-header> <set-header name="Set-Cookie" exists-action="append"> <value>@(context.Variables.GetValueOrDefault<string>("approval_cookie"))</value> </set-header> </return-response> </when> <when condition="@(context.Variables.GetValueOrDefault<string>("consent_action") == "deny")"> <!-- Process consent denial --> <set-variable name="response_status_code" value="403" /> <set-variable name="response_content_type" value="text/html" /> <set-variable name="response_cache_control" value="no-store, no-cache" /> <set-variable name="response_pragma" value="no-cache" /> <!-- Calculate the cookie value right here in inbound before returning response --> <set-variable name="denial_cookie" value="@{ string cookieName = "__Host-MCP_DENIED_CLIENTS"; // Use already extracted parameters instead of re-parsing form data string clientId = context.Variables.GetValueOrDefault<string>("client_id", ""); string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); // Create a unique identifier for this client/redirect combination string clientKey = $"{clientId}:{redirectUri}"; // Check for existing cookie var cookieHeader = context.Request.Headers.GetValueOrDefault("Cookie", ""); JArray deniedClients = new JArray(); if (!string.IsNullOrEmpty(cookieHeader)) { // Parse cookies to find our denial cookie string[] cookies = cookieHeader.Split(';'); foreach (string cookie in cookies) { string trimmedCookie = cookie.Trim(); if (trimmedCookie.StartsWith(cookieName + "=")) { try { // Extract and parse the cookie value string cookieValue = trimmedCookie.Substring(cookieName.Length + 1); // Get the payload part (before the first dot if cookie is signed) string payload = cookieValue.Contains('.') ? cookieValue.Split('.')[0] : cookieValue; string decodedValue = System.Text.Encoding.UTF8.GetString( System.Convert.FromBase64String(payload)); deniedClients = JArray.Parse(decodedValue); } catch (Exception) { // If parsing fails, we'll just create a new cookie deniedClients = new JArray(); } break; } } } // Add the current client if not already in the list bool clientExists = false; foreach (var item in deniedClients) { if (item.ToString() == clientKey) { clientExists = true; break; } } if (!clientExists) { deniedClients.Add(clientKey); } // Base64 encode the client list string jsonClients = deniedClients.ToString(Newtonsoft.Json.Formatting.None); string encodedClients = System.Convert.ToBase64String( System.Text.Encoding.UTF8.GetBytes(jsonClients)); // Return the full cookie string with appropriate settings return $"{cookieName}={encodedClients}; Max-Age=31536000; Path=/; Secure; HttpOnly; SameSite=Lax"; }" /> <!-- Store the HTML content for the access denied page --> <set-variable name="response_body" value="@{ string denialTemplate = context.Variables.GetValueOrDefault<string>("access_denied_template"); string commonStyles = context.Variables.GetValueOrDefault<string>("common_styles"); // Replace placeholders with actual content denialTemplate = denialTemplate.Replace("__COMMON_STYLES__", commonStyles); denialTemplate = denialTemplate.Replace("__DENIAL_MESSAGE__", "You have denied authorization for this application against the MCP server."); return denialTemplate; }" /> <!-- Set variables for outbound policy awareness --> <set-variable name="consent_denied" value="true" /> <set-variable name="cookie_name" value="__Host-MCP_DENIED_CLIENTS" /> <!-- Return the response with the cookie already set --> <return-response> <set-status code="403" reason="Forbidden" /> <set-header name="Content-Type" exists-action="override"> <value>text/html</value> </set-header> <set-header name="Cache-Control" exists-action="override"> <value>no-store, no-cache</value> </set-header> <set-header name="Pragma" exists-action="override"> <value>no-cache</value> </set-header> <set-header name="Set-Cookie" exists-action="append"> <value>@(context.Variables.GetValueOrDefault<string>("denial_cookie"))</value> </set-header> <set-body>@(context.Variables.GetValueOrDefault<string>("response_body", ""))</set-body> </return-response> </when> <otherwise> <!-- Invalid consent action - return error --> <return-response> <set-status code="403" reason="Forbidden" /> <set-header name="Content-Type" exists-action="override"> <value>text/html</value> </set-header> <!-- Explicitly disable any redirects --> <set-header name="Cache-Control" exists-action="override"> <value>no-store, no-cache</value> </set-header> <set-header name="Pragma" exists-action="override"> <value>no-cache</value> </set-header> <set-body>@{ string denialTemplate = context.Variables.GetValueOrDefault<string>("access_denied_template"); string commonStyles = context.Variables.GetValueOrDefault<string>("common_styles"); string consentAction = context.Variables.GetValueOrDefault<string>("consent_action", ""); string detailedMessage = $"Invalid consent action '{consentAction}' received. Expected 'allow' or 'deny'. This may indicate a form tampering attempt or a browser compatibility issue."; // Replace placeholders with actual content denialTemplate = denialTemplate.Replace("__COMMON_STYLES__", commonStyles); denialTemplate = denialTemplate.Replace("__DENIAL_MESSAGE__", detailedMessage); return denialTemplate; }</set-body> </return-response> </otherwise> </choose> </otherwise> </choose> </otherwise> </choose> </otherwise> </choose> </when> <!-- For GET requests, check for cookies first, then display consent page if no cookie found --> <otherwise> <choose> <!-- If there's an approval cookie, skip consent and redirect to authorization endpoint --> <when condition="@(context.Variables.GetValueOrDefault<bool>("has_approval_cookie"))"> <!-- Set redirect location to authorization endpoint --> <set-variable name="response_redirect_location" value="@{ string baseUrl = "{{APIMGatewayURL}}"; string clientId = context.Variables.GetValueOrDefault<string>("client_id", ""); string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); string state = context.Variables.GetValueOrDefault<string>("state", ""); // URL encode parameters to prevent injection attacks string encodedClientId = System.Net.WebUtility.UrlEncode(clientId); string encodedRedirectUri = System.Net.WebUtility.UrlEncode(redirectUri); // State is already properly encoded, don't double-encode string encodedState = state; // Add PKCE parameters if they exist string codeChallenge = context.Variables.GetValueOrDefault<string>("code_challenge", ""); string codeChallengeMethod = context.Variables.GetValueOrDefault<string>("code_challenge_method", ""); string url = $"{baseUrl}/authorize?client_id={encodedClientId}&redirect_uri={encodedRedirectUri}&state={encodedState}"; if (!string.IsNullOrEmpty(codeChallenge)) { url += $"&code_challenge={System.Net.WebUtility.UrlEncode(codeChallenge)}"; } if (!string.IsNullOrEmpty(codeChallengeMethod)) { url += $"&code_challenge_method={System.Net.WebUtility.UrlEncode(codeChallengeMethod)}"; } return url; }" /> <!-- Redirect to authorization endpoint --> <return-response> <set-status code="302" reason="Found" /> <set-header name="Location" exists-action="override"> <value>@(context.Variables.GetValueOrDefault<string>("response_redirect_location", ""))</value> </set-header> </return-response> </when> <!-- If there's a denial cookie, return access denied page immediately --> <when condition="@(context.Variables.GetValueOrDefault<bool>("has_denial_cookie"))"> <return-response> <set-status code="403" reason="Forbidden" /> <set-header name="Content-Type" exists-action="override"> <value>text/html</value> </set-header> <!-- Explicitly disable any redirects --> <set-header name="Cache-Control" exists-action="override"> <value>no-store, no-cache</value> </set-header> <set-header name="Pragma" exists-action="override"> <value>no-cache</value> </set-header> <set-body>@{ string denialTemplate = context.Variables.GetValueOrDefault<string>("access_denied_template"); string commonStyles = context.Variables.GetValueOrDefault<string>("common_styles"); // Replace placeholders with actual content denialTemplate = denialTemplate.Replace("__COMMON_STYLES__", commonStyles); denialTemplate = denialTemplate.Replace("__DENIAL_MESSAGE__", "You have previously denied access to this application."); return denialTemplate; }</set-body> </return-response> </when> <!-- If no cookies found, show the consent screen --> <otherwise> <!-- Check if client is registered first --> <choose> <when condition="@(!context.Variables.GetValueOrDefault<bool>("is_client_registered"))"> <!-- Client is not registered, show error page --> <return-response> <set-status code="403" reason="Forbidden" /> <set-header name="Content-Type" exists-action="override"> <value>text/html</value> </set-header> <set-header name="Cache-Control" exists-action="override"> <value>no-store, no-cache</value> </set-header> <set-header name="Pragma" exists-action="override"> <value>no-cache</value> </set-header> <set-body>@{ string template = context.Variables.GetValueOrDefault<string>("client_not_found_template"); string commonStyles = context.Variables.GetValueOrDefault<string>("common_styles"); string clientId = context.Variables.GetValueOrDefault<string>("client_id", ""); string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); // Replace placeholders with HTML-encoded content to prevent XSS template = template.Replace("__COMMON_STYLES__", commonStyles); template = template.Replace("__CLIENT_ID_DISPLAY__", System.Net.WebUtility.HtmlEncode(clientId)); template = template.Replace("__REDIRECT_URI__", System.Net.WebUtility.HtmlEncode(redirectUri)); return template; }</set-body> </return-response> </when> <otherwise> <!-- Client is registered, get client name from the cache --> <!-- Build consent page using the standardized template --> <set-variable name="consent_page" value="@{ string template = context.Variables.GetValueOrDefault<string>("consent_page_template"); string commonStyles = context.Variables.GetValueOrDefault<string>("common_styles"); // Use the service URL from APIM configuration string basePath = "{{APIMGatewayURL}}"; string clientId = context.Variables.GetValueOrDefault<string>("client_id", ""); string clientName = context.Variables.GetValueOrDefault<string>("client_name", "Unknown Application"); string clientUri = context.Variables.GetValueOrDefault<string>("client_uri", "N/A"); string oauthScopes = context.Variables.GetValueOrDefault<string>("oauth_scopes", ""); // Get the normalized (human-readable) redirect URI for display string normalizedRedirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); // Use the normalized redirect URI for form submission to ensure consistency string formRedirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); string htmlEncodedFormUri = System.Net.WebUtility.HtmlEncode(formRedirectUri); string state = context.Variables.GetValueOrDefault<string>("state", ""); string csrfToken = context.Variables.GetValueOrDefault<string>("csrf_token", ""); // Create a temporary placeholder for the form fields string FORM_FIELD_PLACEHOLDER = "___ENCODED_REDIRECT_URI___"; // Replace the styles first template = template.Replace("__COMMON_STYLES__", commonStyles); // First, create a temporary placeholder for the form fields template = template.Replace("value='__REDIRECT_URI__'", "value='" + FORM_FIELD_PLACEHOLDER + "'"); // Replace template placeholders with properly encoded values template = template.Replace("__CLIENT_NAME__", System.Net.WebUtility.HtmlEncode(clientName)); template = template.Replace("__CLIENT_URI__", System.Net.WebUtility.HtmlEncode(clientUri)); // For display purposes, use HtmlEncode for safety template = template.Replace("__CLIENT_ID_DISPLAY__", System.Net.WebUtility.HtmlEncode(clientId)); template = template.Replace("__REDIRECT_URI__", System.Net.WebUtility.HtmlEncode(normalizedRedirectUri)); // For form field values, use HtmlEncode for XSS protection template = template.Replace("__CLIENT_ID_FORM__", System.Net.WebUtility.HtmlEncode(clientId)); // State should be HTML-encoded for form safety (don't URL-decode first as it may already be in correct format) template = template.Replace("__STATE__", System.Net.WebUtility.HtmlEncode(state)); template = template.Replace("__CODE_CHALLENGE__", System.Net.WebUtility.HtmlEncode(context.Variables.GetValueOrDefault<string>("code_challenge", ""))); template = template.Replace("__CODE_CHALLENGE_METHOD__", System.Net.WebUtility.HtmlEncode(context.Variables.GetValueOrDefault<string>("code_challenge_method", ""))); template = template.Replace("__CSRF_TOKEN__", System.Net.WebUtility.HtmlEncode(csrfToken)); template = template.Replace("__CONSENT_ACTION_URL__", basePath + "/consent"); // Handle space-separated OAuth scopes and create individual list items with HTML encoding string[] scopeArray = oauthScopes.Split(new char[] {' '}, StringSplitOptions.RemoveEmptyEntries); StringBuilder scopeList = new StringBuilder(); foreach (string scope in scopeArray) { scopeList.AppendLine($"<li><code>{System.Net.WebUtility.HtmlEncode(scope)}</code></li>"); } template = template.Replace("__OAUTH_SCOPES__", scopeList.ToString()); // Replace form field placeholder with encoded URI template = template.Replace(FORM_FIELD_PLACEHOLDER, htmlEncodedFormUri); return template; }" /> <!-- Return consent page --> <return-response> <set-status code="200" reason="OK" /> <set-header name="Content-Type" exists-action="override"> <value>text/html</value> </set-header> <!-- Security headers --> <set-header name="X-Frame-Options" exists-action="override"> <value>DENY</value> </set-header> <set-header name="X-Content-Type-Options" exists-action="override"> <value>nosniff</value> </set-header> <set-header name="X-XSS-Protection" exists-action="override"> <value>1; mode=block</value> </set-header> <set-header name="Referrer-Policy" exists-action="override"> <value>strict-origin-when-cross-origin</value> </set-header> <set-header name="Content-Security-Policy" exists-action="override"> <value>default-src 'self'; style-src 'unsafe-inline'; script-src 'none'; object-src 'none'; base-uri 'self'; form-action 'self' https:</value> </set-header> <set-header name="Cache-Control" exists-action="override"> <value>no-store, no-cache, must-revalidate</value> </set-header> <set-header name="Pragma" exists-action="override"> <value>no-cache</value> </set-header> <!-- Store the state parameter in a secure cookie for validation --> <set-header name="Set-Cookie" exists-action="append"> <value>@{ string state = context.Variables.GetValueOrDefault<string>("state", ""); string clientId = context.Variables.GetValueOrDefault<string>("client_id", ""); string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""); // Create consent context data var consentData = new JObject { ["state"] = state, ["clientId"] = clientId, ["redirectUri"] = redirectUri, ["timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") }; // Base64 encode the consent data string consentDataJson = consentData.ToString(Newtonsoft.Json.Formatting.None); string encodedConsentData = System.Convert.ToBase64String( System.Text.Encoding.UTF8.GetBytes(consentDataJson)); return $"__Host-MCP_CONSENT_STATE={encodedConsentData}; Max-Age=900; Path=/; Secure; HttpOnly; SameSite=Lax"; }</value> </set-header> <set-body>@{ return context.Variables.GetValueOrDefault<string>("consent_page", ""); }</set-body> </return-response> </otherwise> </choose> </otherwise> </choose> </otherwise> </choose> </inbound> <backend> <base /> </backend> <outbound> <base /> </outbound> <on-error> <base /> </on-error> </policies>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Azure-Samples/remote-mcp-apim-functions-python'

If you have feedback or need assistance with the MCP directory API, please join our Discord server