Skip to content

Commit 867161e

Browse files
madebygpsCopilot
andauthored
add KEYCLOAK_TOKEN_ISSUER support and filter azd error messages (Azure-Samples#19)
* fix: add KEYCLOAK_TOKEN_ISSUER support and filter azd error messages in .env Add KEYCLOAK_TOKEN_ISSUER to fix JWT validation when Keycloak is behind HTTP routes: - Bicep: Pass keycloakTokenIssuer from main.bicep to server.bicep - Server: Set KEYCLOAK_TOKEN_ISSUER env var with fallback to KEYCLOAK_REALM_URL - Python: Use separate issuer URL for token validation vs realm URL for JWKS Fix write_env scripts to handle azd error output: - azd writes "ERROR: key not found" to stdout instead of stderr - Filter ERROR messages before writing to .env file - Prevents error text from polluting local environment files * update spanish readme * turned error checking into wrapper * Update spanish/README.md Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent aeacc19 commit 867161e

File tree

6 files changed

+524
-75
lines changed

6 files changed

+524
-75
lines changed

infra/main.bicep

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,7 @@ module server 'server.bicep' = {
750750
exists: serverExists
751751
// Keycloak authentication configuration (only when enabled)
752752
keycloakRealmUrl: useKeycloak ? '${keycloak!.outputs.uri}/realms/${keycloakRealmName}' : ''
753+
keycloakTokenIssuer: useKeycloak ? '${keycloakMcpServerBaseUrl}/realms/${keycloakRealmName}' : ''
753754
keycloakMcpServerBaseUrl: useKeycloak ? keycloakMcpServerBaseUrl : ''
754755
keycloakMcpServerAudience: keycloakMcpServerAudience
755756
// Azure/Entra ID OAuth Proxy authentication configuration (only when enabled)
@@ -913,6 +914,7 @@ output KEYCLOAK_MCP_SERVER_BASE_URL string = useKeycloak ? keycloakMcpServerBase
913914
output KEYCLOAK_REALM_URL string = useKeycloak ? '${httpRoutes!.outputs.routeConfigUrl}/auth/realms/${keycloakRealmName}' : ''
914915
output KEYCLOAK_ADMIN_CONSOLE string = useKeycloak ? '${httpRoutes!.outputs.routeConfigUrl}/auth/admin' : ''
915916
output KEYCLOAK_DIRECT_URL string = keycloak.outputs.uri
917+
output KEYCLOAK_TOKEN_ISSUER string = useKeycloak ? '${keycloakMcpServerBaseUrl}/realms/${keycloakRealmName}' : ''
916918

917919
// Auth provider for env scripts
918920
output MCP_AUTH_PROVIDER string = mcpAuthProvider

infra/server.bicep

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ param cosmosDbUserContainer string
1616
param cosmosDbOAuthContainer string
1717
param applicationInsightsConnectionString string = ''
1818
param keycloakRealmUrl string = ''
19+
param keycloakTokenIssuer string = ''
1920
param keycloakMcpServerAudience string = 'mcp-server'
2021
param keycloakMcpServerBaseUrl string = ''
2122
param entraProxyClientId string = ''
@@ -91,6 +92,10 @@ var keycloakEnv = !empty(keycloakRealmUrl) ? [
9192
name: 'KEYCLOAK_REALM_URL'
9293
value: keycloakRealmUrl
9394
}
95+
{
96+
name: 'KEYCLOAK_TOKEN_ISSUER'
97+
value: !empty(keycloakTokenIssuer) ? keycloakTokenIssuer : keycloakRealmUrl
98+
}
9499
{
95100
name: 'KEYCLOAK_MCP_SERVER_AUDIENCE'
96101
value: keycloakMcpServerAudience

infra/write_env.ps1

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,32 @@
11
# Define the .env file path
22
$ENV_FILE_PATH = ".env"
33

4+
5+
# Returns empty string if value not found or contains ERROR
6+
function Get-AzdValue {
7+
param([string]$Key)
8+
$value = azd env get-value $Key 2>$null
9+
if ($value -and $value.Contains("ERROR:")) {
10+
return ""
11+
}
12+
return $value
13+
}
14+
15+
# Write a required env var (always written)
16+
function Write-Env {
17+
param([string]$Key)
18+
Add-Content -Path $ENV_FILE_PATH -Value "$Key=$(Get-AzdValue $Key)"
19+
}
20+
21+
# Write an optional env var (only written if value is non-empty)
22+
function Write-EnvIfSet {
23+
param([string]$Key)
24+
$value = Get-AzdValue $Key
25+
if ($value -and $value -ne "") {
26+
Add-Content -Path $ENV_FILE_PATH -Value "$Key=$value"
27+
}
28+
}
29+
430
# Clear the contents of the .env file
531
Set-Content -Path $ENV_FILE_PATH -Value $null
632

@@ -14,17 +40,21 @@ Add-Content -Path $ENV_FILE_PATH -Value "AZURE_COSMOSDB_CONTAINER=$(azd env get-
1440
Add-Content -Path $ENV_FILE_PATH -Value "AZURE_COSMOSDB_USER_CONTAINER=$(azd env get-value AZURE_COSMOSDB_USER_CONTAINER)"
1541
Add-Content -Path $ENV_FILE_PATH -Value "AZURE_COSMOSDB_OAUTH_CONTAINER=$(azd env get-value AZURE_COSMOSDB_OAUTH_CONTAINER)"
1642
Add-Content -Path $ENV_FILE_PATH -Value "APPLICATIONINSIGHTS_CONNECTION_STRING=$(azd env get-value APPLICATIONINSIGHTS_CONNECTION_STRING)"
17-
Add-Content -Path $ENV_FILE_PATH -Value "MCP_AUTH_PROVIDER=$(azd env get-value MCP_AUTH_PROVIDER)"
18-
$KEYCLOAK_REALM_URL = azd env get-value KEYCLOAK_REALM_URL 2>$null
43+
Write-Env MCP_AUTH_PROVIDER
44+
45+
# Keycloak-related env vars (only if KEYCLOAK_REALM_URL is set)
46+
$KEYCLOAK_REALM_URL = Get-AzdValue KEYCLOAK_REALM_URL
1947
if ($KEYCLOAK_REALM_URL -and $KEYCLOAK_REALM_URL -ne "") {
2048
Add-Content -Path $ENV_FILE_PATH -Value "KEYCLOAK_REALM_URL=$KEYCLOAK_REALM_URL"
49+
Write-EnvIfSet KEYCLOAK_TOKEN_ISSUER
2150
}
22-
$ENTRA_PROXY_AZURE_CLIENT_ID = azd env get-value ENTRA_PROXY_AZURE_CLIENT_ID 2>$null
51+
52+
# Entra proxy env vars (only if ENTRA_PROXY_AZURE_CLIENT_ID is set)
53+
$ENTRA_PROXY_AZURE_CLIENT_ID = Get-AzdValue ENTRA_PROXY_AZURE_CLIENT_ID
2354
if ($ENTRA_PROXY_AZURE_CLIENT_ID -and $ENTRA_PROXY_AZURE_CLIENT_ID -ne "") {
2455
Add-Content -Path $ENV_FILE_PATH -Value "ENTRA_PROXY_AZURE_CLIENT_ID=$ENTRA_PROXY_AZURE_CLIENT_ID"
25-
Add-Content -Path $ENV_FILE_PATH -Value "ENTRA_PROXY_AZURE_CLIENT_SECRET=$(azd env get-value ENTRA_PROXY_AZURE_CLIENT_SECRET)"
26-
$ENTRA_PROXY_MCP_SERVER_BASE_URL = azd env get-value ENTRA_PROXY_MCP_SERVER_BASE_URL 2>$null
27-
Add-Content -Path $ENV_FILE_PATH -Value "ENTRA_PROXY_MCP_SERVER_BASE_URL=$ENTRA_PROXY_MCP_SERVER_BASE_URL"
56+
Write-Env ENTRA_PROXY_AZURE_CLIENT_SECRET
57+
Write-Env ENTRA_PROXY_MCP_SERVER_BASE_URL
2858
}
2959
Add-Content -Path $ENV_FILE_PATH -Value "MCP_ENTRY=$(azd env get-value MCP_ENTRY)"
3060
Add-Content -Path $ENV_FILE_PATH -Value "MCP_SERVER_URL=$(azd env get-value MCP_SERVER_URL)"

infra/write_env.sh

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,34 @@ set -e
55
# Define the .env file path
66
ENV_FILE_PATH=".env"
77

8+
# Returns empty string if value not found or contains ERROR
9+
get_azd_value() {
10+
local key="$1"
11+
local value
12+
value=$(azd env get-value "$key" 2>/dev/null || echo "")
13+
if [[ "$value" == *ERROR:* ]]; then
14+
echo ""
15+
else
16+
echo "$value"
17+
fi
18+
}
19+
20+
# Write a required env var (always written)
21+
write_env() {
22+
local key="$1"
23+
echo "${key}=$(get_azd_value "$key")" >> "$ENV_FILE_PATH"
24+
}
25+
26+
# Write an optional env var (only written if value is non-empty)
27+
write_env_if_set() {
28+
local key="$1"
29+
local value
30+
value=$(get_azd_value "$key")
31+
if [ -n "$value" ]; then
32+
echo "${key}=${value}" >> "$ENV_FILE_PATH"
33+
fi
34+
}
35+
836
# Clear the contents of the .env file
937
> "$ENV_FILE_PATH"
1038

@@ -18,16 +46,21 @@ echo "AZURE_COSMOSDB_CONTAINER=$(azd env get-value AZURE_COSMOSDB_CONTAINER)" >>
1846
echo "AZURE_COSMOSDB_USER_CONTAINER=$(azd env get-value AZURE_COSMOSDB_USER_CONTAINER)" >> "$ENV_FILE_PATH"
1947
echo "AZURE_COSMOSDB_OAUTH_CONTAINER=$(azd env get-value AZURE_COSMOSDB_OAUTH_CONTAINER)" >> "$ENV_FILE_PATH"
2048
echo "APPLICATIONINSIGHTS_CONNECTION_STRING=$(azd env get-value APPLICATIONINSIGHTS_CONNECTION_STRING)" >> "$ENV_FILE_PATH"
21-
echo "MCP_AUTH_PROVIDER=$(azd env get-value MCP_AUTH_PROVIDER)" >> "$ENV_FILE_PATH"
22-
KEYCLOAK_REALM_URL=$(azd env get-value KEYCLOAK_REALM_URL 2>/dev/null || echo "")
23-
if [ -n "$KEYCLOAK_REALM_URL" ] && [ "$KEYCLOAK_REALM_URL" != "" ]; then
49+
write_env MCP_AUTH_PROVIDER
50+
51+
# Keycloak-related env vars (only if KEYCLOAK_REALM_URL is set)
52+
KEYCLOAK_REALM_URL=$(get_azd_value KEYCLOAK_REALM_URL)
53+
if [ -n "$KEYCLOAK_REALM_URL" ]; then
2454
echo "KEYCLOAK_REALM_URL=${KEYCLOAK_REALM_URL}" >> "$ENV_FILE_PATH"
55+
write_env_if_set KEYCLOAK_TOKEN_ISSUER
2556
fi
26-
ENTRA_PROXY_AZURE_CLIENT_ID=$(azd env get-value ENTRA_PROXY_AZURE_CLIENT_ID 2>/dev/null || echo "")
27-
if [ -n "$ENTRA_PROXY_AZURE_CLIENT_ID" ] && [ "$ENTRA_PROXY_AZURE_CLIENT_ID" != "" ]; then
57+
58+
# Entra proxy env vars (only if ENTRA_PROXY_AZURE_CLIENT_ID is set)
59+
ENTRA_PROXY_AZURE_CLIENT_ID=$(get_azd_value ENTRA_PROXY_AZURE_CLIENT_ID)
60+
if [ -n "$ENTRA_PROXY_AZURE_CLIENT_ID" ]; then
2861
echo "ENTRA_PROXY_AZURE_CLIENT_ID=${ENTRA_PROXY_AZURE_CLIENT_ID}" >> "$ENV_FILE_PATH"
29-
echo "ENTRA_PROXY_AZURE_CLIENT_SECRET=$(azd env get-value ENTRA_PROXY_AZURE_CLIENT_SECRET)" >> "$ENV_FILE_PATH"
30-
echo "ENTRA_PROXY_MCP_SERVER_BASE_URL=$(azd env get-value ENTRA_PROXY_MCP_SERVER_BASE_URL)" >> "$ENV_FILE_PATH"
62+
write_env ENTRA_PROXY_AZURE_CLIENT_SECRET
63+
write_env ENTRA_PROXY_MCP_SERVER_BASE_URL
3164
fi
3265
echo "MCP_ENTRY=$(azd env get-value MCP_ENTRY)" >> "$ENV_FILE_PATH"
3366
echo "MCP_SERVER_URL=$(azd env get-value MCP_SERVER_URL)" >> "$ENV_FILE_PATH"

servers/auth_mcp.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,10 @@
102102
elif mcp_auth_provider == "keycloak":
103103
# Keycloak authentication using RemoteAuthProvider with JWT verification
104104
KEYCLOAK_REALM_URL = os.environ["KEYCLOAK_REALM_URL"]
105+
KEYCLOAK_TOKEN_ISSUER = os.getenv("KEYCLOAK_TOKEN_ISSUER", KEYCLOAK_REALM_URL)
105106
token_verifier = JWTVerifier(
106107
jwks_uri=f"{KEYCLOAK_REALM_URL}/protocol/openid-connect/certs",
107-
issuer=KEYCLOAK_REALM_URL,
108+
issuer=KEYCLOAK_TOKEN_ISSUER,
108109
audience=os.getenv("KEYCLOAK_MCP_SERVER_AUDIENCE", "mcp-server"),
109110
)
110111
# Prefer specific base URL env for Keycloak when provided

0 commit comments

Comments
 (0)