Skip to main content
Glama
Azure-Samples

Secure Remote MCP Server

oauth-callback.policy.xml13.4 kB
<!-- OAUTH CALLBACK POLICY This policy implements the callback endpoint for PKCE OAuth2 flow with Entra ID. --> <policies> <inbound> <base /> <!-- STEP 1: Extract the authorization code and state from Entra ID callback --> <set-variable name="authCode" value="@((string)context.Request.Url.Query.GetValueOrDefault("code", ""))" /> <set-variable name="clientState" value="@{ string stateValue = (string)context.Request.Url.Query.GetValueOrDefault("state", ""); return !string.IsNullOrEmpty(stateValue) ? System.Net.WebUtility.UrlDecode(stateValue) : ""; }" /> <set-variable name="sessionState" value="@((string)context.Request.Url.Query.GetValueOrDefault("session_state", ""))" /> <!-- Validate required OAuth parameters --> <choose> <when condition="@(string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault("authCode", "")) || string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault("clientState", "")))"> <return-response> <set-status code="400" reason="Bad Request" /> <set-header name="Content-Type" exists-action="override"> <value>application/json</value> </set-header> <set-body>@{ var errorResponse = new JObject(); errorResponse["error"] = "invalid_request"; errorResponse["error_description"] = "Missing required OAuth callback parameters"; return errorResponse.ToString(); }</set-body> </return-response> </when> </choose> <!-- STEP 1.5: Validate that the state matches what the user consented to --> <set-variable name="consent_state_valid" value="@{ try { string returnedState = context.Variables.GetValueOrDefault<string>("clientState", ""); if (string.IsNullOrEmpty(returnedState)) { return false; } // Extract consent state from cookie var cookieHeader = context.Request.Headers.GetValueOrDefault("Cookie", ""); if (string.IsNullOrEmpty(cookieHeader)) { return false; } string cookieName = "__Host-MCP_CONSENT_STATE"; string[] cookies = cookieHeader.Split(';'); foreach (string cookie in cookies) { string trimmedCookie = cookie.Trim(); if (trimmedCookie.StartsWith(cookieName + "=")) { string cookieValue = trimmedCookie.Substring(cookieName.Length + 1); string decodedValue = System.Text.Encoding.UTF8.GetString( System.Convert.FromBase64String(cookieValue)); JObject consentData = JObject.Parse(decodedValue); string consentedState = consentData["state"]?.ToString(); // Constant-time comparison to prevent timing attacks if (string.IsNullOrEmpty(consentedState) || returnedState.Length != consentedState.Length) { return false; } int result = 0; for (int i = 0; i < returnedState.Length; i++) { result |= returnedState[i] ^ consentedState[i]; } return (result == 0); } } return false; } catch (Exception ex) { return false; } }" /> <!-- Validate consent state cookie --> <choose> <when condition="@(!context.Variables.GetValueOrDefault<bool>("consent_state_valid"))"> <return-response> <set-status code="400" reason="Bad Request" /> <set-header name="Content-Type" exists-action="override"> <value>application/json</value> </set-header> <set-body>@{ var errorResponse = new JObject(); errorResponse["error"] = "invalid_state"; errorResponse["error_description"] = "State parameter does not match consented state."; return errorResponse.ToString(); }</set-body> </return-response> </when> </choose> <!-- Clear the consent state cookie since it's been validated --> <set-variable name="clear_consent_cookie" value="__Host-MCP_CONSENT_STATE=; Max-Age=0; Path=/; Secure; HttpOnly; SameSite=Lax" /> <!-- STEP 2: Retrieve stored PKCE code verifier using the client state parameter --> <cache-lookup-value key="@("CodeVerifier-"+context.Variables.GetValueOrDefault("clientState", ""))" variable-name="codeVerifier" /> <!-- Validate that code verifier was found in cache --> <choose> <when condition="@(string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault("codeVerifier", "")))"> <return-response> <set-status code="400" reason="Bad Request" /> <set-header name="Content-Type" exists-action="override"> <value>application/json</value> </set-header> <set-body>@{ var errorResponse = new JObject(); errorResponse["error"] = "invalid_request"; errorResponse["error_description"] = "Authorization session expired or invalid state parameter"; return errorResponse.ToString(); }</set-body> </return-response> </when> </choose> <!-- STEP 3: Set token request parameters --> <set-variable name="codeChallengeMethod" value="S256" /> <set-variable name="redirectUri" value="{{OAuthCallbackUri}}" /> <set-variable name="clientId" value="{{EntraIDClientId}}" /> <set-variable name="clientAssertionType" value="@(System.Net.WebUtility.UrlEncode("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"))" /> <authentication-managed-identity resource="api://AzureADTokenExchange" client-id="{{EntraIDFicClientId}}" output-token-variable-name="ficToken"/> <!-- STEP 4: Configure token request to Entra ID --> <set-method>POST</set-method> <set-header name="Content-Type" exists-action="override"> <value>application/x-www-form-urlencoded</value> </set-header> <set-body>@{ return $"client_id={context.Variables.GetValueOrDefault("clientId")}&grant_type=authorization_code&code={context.Variables.GetValueOrDefault("authCode")}&redirect_uri={context.Variables.GetValueOrDefault("redirectUri")}&scope=User.Read&code_verifier={context.Variables.GetValueOrDefault("codeVerifier")}&client_assertion_type={context.Variables.GetValueOrDefault("clientAssertionType")}&client_assertion={context.Variables.GetValueOrDefault("ficToken")}"; }</set-body> <rewrite-uri template="/token" /> </inbound> <backend> <base /> </backend> <outbound> <base /> <!-- STEP 5: Process the token response from Entra ID --> <trace source="apim-policy"> <message>@("Token response received: " + context.Response.Body.As<string>(preserveContent: true))</message> </trace> <!-- Check if the response is successful (200 OK) and contains a token --> <choose> <when condition="@(context.Response.StatusCode != 200 || string.IsNullOrEmpty(context.Response.Body.As<JObject>(preserveContent: true)["access_token"]?.ToString()))"> <return-response> <set-status code="@(context.Response.StatusCode)" reason="@(context.Response.StatusReason)" /> <set-header name="Content-Type" exists-action="override"> <value>application/json</value> </set-header> <set-body>@{ var errorResponse = new JObject(); errorResponse["error"] = "token_error"; errorResponse["error_description"] = "Failed to retrieve access token from Entra ID."; return errorResponse.ToString(); }</set-body> </return-response> </when> </choose> <!-- STEP 6: Generate secure session token for MCP client --> <set-variable name="IV" value="{{EncryptionIV}}" /> <set-variable name="key" value="{{EncryptionKey}}" /> <set-variable name="sessionId" value="@((string)Guid.NewGuid().ToString().Replace("-", ""))" /> <set-variable name="encryptedSessionKey" value="@{ // Generate a unique session ID string sessionId = (string)context.Variables.GetValueOrDefault("sessionId"); byte[] sessionIdBytes = Encoding.UTF8.GetBytes(sessionId); // Encrypt the session ID using AES byte[] IV = Convert.FromBase64String((string)context.Variables["IV"]); byte[] key = Convert.FromBase64String((string)context.Variables["key"]); byte[] encryptedBytes = sessionIdBytes.Encrypt("Aes", key, IV); return Convert.ToBase64String(encryptedBytes); }" /> <!-- STEP 6: Lookup MCP client redirect URI stored during authorization --> <cache-lookup-value key="@((string)context.Variables.GetValueOrDefault("clientState"))" variable-name="mcpConfirmConsentCode" /> <cache-lookup-value key="@($"McpClientAuthData-{context.Variables.GetValueOrDefault("mcpConfirmConsentCode")}")" variable-name="mcpClientData" /> <!-- Validate that MCP client data was found in cache --> <choose> <when condition="@(string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault("mcpConfirmConsentCode", "")) || string.IsNullOrEmpty((string)context.Variables.GetValueOrDefault("mcpClientData", "")))"> <return-response> <set-status code="400" reason="Bad Request" /> <set-header name="Content-Type" exists-action="override"> <value>application/json</value> </set-header> <set-body>@{ var errorResponse = new JObject(); errorResponse["error"] = "invalid_request"; errorResponse["error_description"] = "MCP client authorization session expired or invalid"; return errorResponse.ToString(); }</set-body> </return-response> </when> </choose> <!-- STEP 8: Use the client's original state parameter directly --> <set-variable name="mcpState" value="@(context.Variables.GetValueOrDefault<string>("clientState"))" /> <!-- STEP 9: Extract the stored mcp client callback redirect uri from cache --> <set-variable name="callbackRedirectUri" value="@{ var mcpAuthDataAsJObject = JObject.Parse((string)context.Variables["mcpClientData"]); return mcpAuthDataAsJObject["mcpCallbackRedirectUri"]; }" /> <!-- STEP 10: Store the encrypted session key and Entra token in cache --> <!-- Store the encrypted session key with the MCP confirmation code as key --> <cache-store-value duration="3600" key="@($"AccessToken-{context.Variables.GetValueOrDefault("mcpConfirmConsentCode")}")" value="@($"{context.Variables.GetValueOrDefault("encryptedSessionKey")}")" /> <!-- Store the Entra token for later use --> <cache-store-value duration="3600" key="@($"EntraToken-{context.Variables.GetValueOrDefault("sessionId")}")" value="@(context.Response.Body.As<JObject>(preserveContent: true).ToString())" /> <!-- STEP 11: Redirect back to MCP client with confirmation code --> <return-response> <set-status code="302" reason="Found" /> <set-header name="Location" exists-action="override"> <value>@($"{context.Variables.GetValueOrDefault("callbackRedirectUri")}?code={context.Variables.GetValueOrDefault("mcpConfirmConsentCode")}&state={System.Net.WebUtility.UrlEncode((string)context.Variables.GetValueOrDefault("mcpState"))}")</value> </set-header> <!-- Clear the consent state cookie --> <set-header name="Set-Cookie" exists-action="append"> <value>@(context.Variables.GetValueOrDefault<string>("clear_consent_cookie"))</value> </set-header> <set-body /> </return-response> </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