<!--
AUTHORIZE POLICY
OAuth 2.0 PKCE authorization endpoint with Entra ID integration.
Flow: Client → Consent (if needed) → Entra ID → Callback → Client
-->
<policies>
<inbound>
<base />
<!-- Extract all OAuth parameters -->
<set-variable name="clientId" value="@((string)context.Request.Url.Query.GetValueOrDefault("client_id", ""))" />
<set-variable name="redirect_uri" value="@((string)context.Request.Url.Query.GetValueOrDefault("redirect_uri", ""))" />
<set-variable name="currentState" value="@((string)context.Request.Url.Query.GetValueOrDefault("state", ""))" />
<set-variable name="mcpScope" value="@((string)context.Request.Url.Query.GetValueOrDefault("scope", ""))" />
<set-variable name="mcpClientCodeChallenge" value="@((string)context.Request.Url.Query.GetValueOrDefault("code_challenge", ""))" />
<set-variable name="mcpClientCodeChallengeMethod" value="@((string)context.Request.Url.Query.GetValueOrDefault("code_challenge_method", ""))" />
<!-- Validate required OAuth parameters -->
<choose>
<when condition="@(string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("clientId")) ||
string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("redirect_uri")) ||
string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("currentState")))">
<return-response>
<set-status code="400" reason="Bad Request" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="Cache-Control" exists-action="override">
<value>no-store, no-cache</value>
</set-header>
<set-body>@{
return new JObject {
["error"] = "invalid_request",
["error_description"] = "Missing required parameters: client_id, redirect_uri, and state are all required for OAuth authorization"
}.ToString();
}</set-body>
</return-response>
</when>
</choose>
<!-- Validate required PKCE parameters -->
<choose>
<when condition="@(string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("mcpClientCodeChallenge")) ||
string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("mcpClientCodeChallengeMethod")))">
<return-response>
<set-status code="400" reason="Bad Request" />
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
<set-header name="Cache-Control" exists-action="override">
<value>no-store, no-cache</value>
</set-header>
<set-body>@{
return new JObject {
["error"] = "invalid_request",
["error_description"] = "Missing required PKCE parameters: code_challenge and code_challenge_method are required for secure authorization"
}.ToString();
}</set-body>
</return-response>
</when>
</choose>
<!-- Normalize redirect URI -->
<set-variable name="normalized_redirect_uri" value="@{
string redirectUri = context.Variables.GetValueOrDefault<string>("redirect_uri", "");
if (string.IsNullOrEmpty(redirectUri)) {
return "";
}
try {
string decodedUri = System.Net.WebUtility.UrlDecode(redirectUri);
return decodedUri;
} catch (Exception) {
return redirectUri;
}
}" />
<!-- Check for existing approval cookie -->
<set-variable name="has_approval_cookie" value="@{
try {
if (string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("clientId", "")) ||
string.IsNullOrEmpty(context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", ""))) {
return false;
}
string clientId = context.Variables.GetValueOrDefault<string>("clientId", "");
string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri", "");
string APPROVAL_COOKIE_NAME = "__Host-MCP_APPROVED_CLIENTS";
var cookieHeader = context.Request.Headers.GetValueOrDefault("Cookie", "");
if (string.IsNullOrEmpty(cookieHeader)) {
return false;
}
string[] cookies = cookieHeader.Split(';');
foreach (string cookie in cookies) {
string trimmedCookie = cookie.Trim();
if (trimmedCookie.StartsWith(APPROVAL_COOKIE_NAME + "=")) {
try {
string cookieValue = trimmedCookie.Substring(APPROVAL_COOKIE_NAME.Length + 1);
string decodedValue = System.Text.Encoding.UTF8.GetString(
System.Convert.FromBase64String(cookieValue));
JArray approvedClients = JArray.Parse(decodedValue);
string clientKey = $"{clientId}:{redirectUri}";
foreach (var item in approvedClients) {
if (item.ToString() == clientKey) {
return true;
}
}
} catch (Exception ex) {
// Error parsing approval cookie - ignore and continue
}
break;
}
}
return false;
} catch (Exception ex) {
// Error checking approval cookie - return false
return false;
}
}" />
<!-- Check if the client has been approved via secure cookie -->
<choose>
<when condition="@(context.Variables.GetValueOrDefault<bool>("has_approval_cookie"))">
<!-- Continue with normal flow - client is authorized via secure cookie -->
</when>
<otherwise>
<!-- Redirect to consent page for user approval -->
<return-response>
<set-status code="302" reason="Found" />
<set-header name="Location" exists-action="override">
<value>@{
string basePath = context.Request.OriginalUrl.Scheme + "://" + context.Request.OriginalUrl.Host + (context.Request.OriginalUrl.Port == 80 || context.Request.OriginalUrl.Port == 443 ? "" : ":" + context.Request.OriginalUrl.Port);
string clientId = context.Variables.GetValueOrDefault<string>("clientId");
// Use the normalized (already decoded) redirect_uri to avoid double-encoding
string redirectUri = context.Variables.GetValueOrDefault<string>("normalized_redirect_uri");
string state = context.Variables.GetValueOrDefault<string>("currentState");
string codeChallenge = context.Variables.GetValueOrDefault<string>("mcpClientCodeChallenge");
string codeChallengeMethod = context.Variables.GetValueOrDefault<string>("mcpClientCodeChallengeMethod");
// URL encode parameters for the consent redirect URL
string encodedClientId = System.Net.WebUtility.UrlEncode(clientId);
string encodedRedirectUri = System.Net.WebUtility.UrlEncode(redirectUri);
// State parameter: use as-is without additional encoding
// context.Request.Url.Query.GetValueOrDefault() preserves the original encoding
string encodedState = state;
// Code challenge parameters: use as-is since they typically don't need encoding
string encodedCodeChallenge = codeChallenge;
string encodedCodeChallengeMethod = codeChallengeMethod;
return $"{basePath}/consent?client_id={encodedClientId}&redirect_uri={encodedRedirectUri}&state={encodedState}&code_challenge={encodedCodeChallenge}&code_challenge_method={encodedCodeChallengeMethod}";
}</value>
</set-header>
</return-response>
</otherwise>
</choose>
<set-variable name="codeVerifier" value="@((string)Guid.NewGuid().ToString().Replace("-", ""))" />
<set-variable name="codeChallenge" value="@{
using (var sha256 = System.Security.Cryptography.SHA256.Create())
{
var bytes = System.Text.Encoding.UTF8.GetBytes((string)context.Variables.GetValueOrDefault("codeVerifier", ""));
var hash = sha256.ComputeHash(bytes);
return System.Convert.ToBase64String(hash).TrimEnd('=').Replace('+', '-').Replace('/', '_');
}
}" />
<!-- Build the complete Entra ID URL using client's original state -->
<set-variable name="authUrl" value="@{
string baseUrl = "https://login.microsoftonline.com/{{EntraIDTenantId}}/oauth2/v2.0/authorize";
string codeChallenge = context.Variables.GetValueOrDefault("codeChallenge", "");
string clientState = context.Variables.GetValueOrDefault("currentState", "");
return $"{baseUrl}?response_type=code&client_id={{EntraIDClientId}}&redirect_uri={{OAuthCallbackUri}}&scope={{OAuthScopes}}&code_challenge={codeChallenge}&code_challenge_method=S256&state={System.Net.WebUtility.UrlEncode(clientState)}";
}" />
<!-- STEP 5: Store authentication data in cache for use in callback -->
<!-- Generate a confirmation code to return to the MCP client -->
<set-variable name="mcpConfirmConsentCode" value="@((string)Guid.NewGuid().ToString())" />
<!-- Store code verifier for token exchange using client state -->
<cache-store-value duration="3600"
key="@("CodeVerifier-"+context.Variables.GetValueOrDefault("currentState", ""))"
value="@(context.Variables.GetValueOrDefault("codeVerifier", ""))" />
<!-- Map client state to MCP confirmation code for callback -->
<cache-store-value duration="3600"
key="@((string)context.Variables.GetValueOrDefault("currentState"))"
value="@(context.Variables.GetValueOrDefault("mcpConfirmConsentCode", ""))" />
<!-- Store MCP client data -->
<cache-store-value duration="3600"
key="@($"McpClientAuthData-{context.Variables.GetValueOrDefault("mcpConfirmConsentCode")}")"
value="@{
return new JObject{
["mcpClientCodeChallenge"] = (string)context.Variables["mcpClientCodeChallenge"],
["mcpClientCodeChallengeMethod"] = (string)context.Variables["mcpClientCodeChallengeMethod"],
["mcpClientState"] = (string)context.Variables["currentState"],
["mcpClientScope"] = (string)context.Variables["mcpScope"],
["mcpCallbackRedirectUri"] = (string)context.Variables["normalized_redirect_uri"]
}.ToString();
}" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<!-- Return the response with a 302 status code for redirect -->
<return-response>
<set-status code="302" reason="Found" />
<set-header name="Location" exists-action="override">
<value>@(context.Variables.GetValueOrDefault("authUrl", ""))</value>
</set-header>
<!-- Add cache control headers to ensure browser follows redirect -->
<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>
<!-- Remove any content-type that might interfere -->
<set-header name="Content-Type" exists-action="delete" />
</return-response>
</outbound>
<on-error>
<base />
</on-error>
</policies>