<?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>