Skip to content

Commit f47875e

Browse files
authored
Merge pull request #991 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents 585e390 + 2350636 commit f47875e

3 files changed

Lines changed: 131 additions & 5 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
function Set-CIPPMCPClientApp {
2+
<#
3+
.SYNOPSIS
4+
Configures an API client's app registration to act as the MCP OAuth resource.
5+
.DESCRIPTION
6+
Sets a cipp API client.
7+
.PARAMETER AppId
8+
Application (client) ID of the API client to configure.
9+
.FUNCTIONALITY
10+
Internal
11+
#>
12+
[CmdletBinding(SupportsShouldProcess)]
13+
param(
14+
[Parameter(Mandatory)]
15+
[string]$AppId,
16+
$Headers
17+
)
18+
19+
$Hostname = $env:WEBSITE_HOSTNAME
20+
if ([string]::IsNullOrWhiteSpace($Hostname)) {
21+
throw 'WEBSITE_HOSTNAME is not set; cannot determine the MCP resource URL.'
22+
}
23+
24+
$McpUris = @("https://$Hostname", "https://$Hostname/api/ExecMcp")
25+
26+
$App = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$AppId')" -NoAuthCheck $true -AsApp $true
27+
if (-not $App) {
28+
throw "App registration with AppId '$AppId' was not found."
29+
}
30+
31+
# Merge identifier URIs, preserving existing (e.g. api://<appId>)
32+
$IdentifierUris = [System.Collections.Generic.List[string]]::new()
33+
foreach ($Uri in @($App.identifierUris)) {
34+
if (-not [string]::IsNullOrWhiteSpace($Uri) -and $IdentifierUris -notcontains $Uri) { $IdentifierUris.Add($Uri) }
35+
}
36+
foreach ($Uri in $McpUris) {
37+
if ($IdentifierUris -notcontains $Uri) { $IdentifierUris.Add($Uri) }
38+
}
39+
40+
# Preserve the existing api object; force v2 tokens; ensure a user_impersonation delegated scope
41+
$Api = if ($App.api) { $App.api | ConvertTo-Json -Depth 10 | ConvertFrom-Json -AsHashtable } else { @{} }
42+
$Api.requestedAccessTokenVersion = 2
43+
$Scopes = [System.Collections.Generic.List[object]]::new()
44+
if ($Api.oauth2PermissionScopes) {
45+
foreach ($Scope in $Api.oauth2PermissionScopes) { $Scopes.Add($Scope) }
46+
}
47+
if (-not ($Scopes | Where-Object { $_.value -eq 'user_impersonation' })) {
48+
$Scopes.Add(@{
49+
adminConsentDescription = 'Allow the application to access CIPP-API on behalf of the signed-in user.'
50+
adminConsentDisplayName = 'Access CIPP-API'
51+
id = [guid]::NewGuid().ToString()
52+
isEnabled = $true
53+
type = 'User'
54+
userConsentDescription = 'Allow the application to access CIPP-API on your behalf.'
55+
userConsentDisplayName = 'Access CIPP-API'
56+
value = 'user_impersonation'
57+
})
58+
}
59+
$Api.oauth2PermissionScopes = @($Scopes)
60+
61+
$PatchBody = @{
62+
identifierUris = @($IdentifierUris)
63+
api = $Api
64+
} | ConvertTo-Json -Depth 10 -Compress
65+
66+
if ($PSCmdlet.ShouldProcess($AppId, 'Configure app registration for MCP')) {
67+
try {
68+
$null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($App.id)" -type PATCH -body $PatchBody -NoAuthCheck $true -asapp $true
69+
Write-LogMessage -headers $Headers -API 'ExecApiClient' -message "Configured app registration $AppId as MCP resource (identifier URIs + v2 tokens)." -Sev 'Info'
70+
return @{ Success = $true; IdentifierUris = @($IdentifierUris) }
71+
} catch {
72+
$ErrMsg = $_.Exception.Message
73+
if ($ErrMsg -match 'identifierUri' -or $ErrMsg -match 'already exists' -or $ErrMsg -match 'in use') {
74+
throw "The MCP resource URIs are already assigned to another application. Only one API client can be the MCP resource client. ($ErrMsg)"
75+
}
76+
throw
77+
}
78+
}
79+
}

Modules/CIPPCore/Public/Authentication/Set-CippApiAuth.ps1

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ function Set-CippApiAuth {
44
[string]$RGName,
55
[string]$FunctionAppName,
66
[string]$TenantId,
7-
[string[]]$ClientIds
7+
[string[]]$ClientIds,
8+
[string[]]$McpClientIds
89
)
910

1011
if ($env:CIPPNG) {
@@ -18,6 +19,7 @@ function Set-CippApiAuth {
1819

1920
Write-Information "[ApiAuth] SiteName=$SiteName, ResourceGroup=$ResourceGroup, SubscriptionId=$SubscriptionId"
2021
Write-Information "[ApiAuth] ClientIds to set: $($ClientIds -join ', ')"
22+
Write-Information "[ApiAuth] MCP client IDs: $($McpClientIds -join ', ') | WEBSITE_HOSTNAME=$($env:WEBSITE_HOSTNAME)"
2123

2224
if (-not $SiteName -or -not $ResourceGroup -or -not $SubscriptionId) {
2325
throw "[ApiAuth] Missing App Service env vars: WEBSITE_SITE_NAME=$SiteName, WEBSITE_RESOURCE_GROUP=$ResourceGroup, SubscriptionId=$SubscriptionId"
@@ -63,6 +65,16 @@ function Set-CippApiAuth {
6365
[void]$AllAudiences.Add("api://$id")
6466
}
6567

68+
# MCP resource clients also accept tokens whose audience is the host-based identifier URI or
69+
# the bare appId (v2 tokens), so the Claude connector's token validates against EasyAuth.
70+
if ($McpClientIds -and $env:WEBSITE_HOSTNAME) {
71+
[void]$AllAudiences.Add("https://$($env:WEBSITE_HOSTNAME)")
72+
[void]$AllAudiences.Add("https://$($env:WEBSITE_HOSTNAME)/api/ExecMcp")
73+
foreach ($McpId in $McpClientIds) {
74+
if (-not [string]::IsNullOrEmpty($McpId)) { [void]$AllAudiences.Add($McpId) }
75+
}
76+
}
77+
6678
Write-Information "[ApiAuth] Merged allowedApplications: $($AllAppIds -join ', ')"
6779
Write-Information "[ApiAuth] Merged allowedAudiences: $($AllAudiences -join ', ')"
6880

@@ -104,10 +116,19 @@ function Set-CippApiAuth {
104116

105117
Write-Information "AuthSettings: $($AuthSettings | ConvertTo-Json -Depth 10)"
106118

107-
# Set allowed audiences
108-
$AllowedAudiences = foreach ($ClientId in $ClientIds) {
109-
"api://$ClientId"
119+
# Set allowed audiences (api://{id} for each, plus MCP resource URIs + bare appId for MCP clients)
120+
$AudienceList = [System.Collections.Generic.List[string]]::new()
121+
foreach ($ClientId in $ClientIds) {
122+
$AudienceList.Add("api://$ClientId")
123+
}
124+
if ($McpClientIds -and $env:WEBSITE_HOSTNAME) {
125+
$AudienceList.Add("https://$($env:WEBSITE_HOSTNAME)")
126+
$AudienceList.Add("https://$($env:WEBSITE_HOSTNAME)/api/ExecMcp")
127+
foreach ($McpId in $McpClientIds) {
128+
if (-not [string]::IsNullOrEmpty($McpId)) { $AudienceList.Add($McpId) }
129+
}
110130
}
131+
$AllowedAudiences = @($AudienceList)
111132

112133
if (!$AllowedAudiences) { $AllowedAudiences = @() }
113134
if (!$ClientIds) { $ClientIds = @() }

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@ function Invoke-ExecApiClient {
132132
}
133133

134134
Add-CIPPAzDataTableEntity @Table -Entity $Client -Force | Out-Null
135+
136+
# When this client is MCP-enabled, configure its app registration as the MCP OAuth
137+
# resource (host identifier URIs + v2 tokens) so the Claude connector flow can resolve it.
138+
if ([bool]($Request.Body.MCPAllowed ?? $false)) {
139+
try {
140+
$null = Set-CIPPMCPClientApp -AppId $ClientId -Headers $Request.Headers
141+
$Results.Add('MCP resource URIs and v2 tokens configured on the app registration. Run Save to Azure to apply the changes.')
142+
} catch {
143+
$Results.Add(@{
144+
resultText = "Client saved, but MCP app configuration failed: $($_.Exception.Message)"
145+
state = 'warning'
146+
})
147+
}
148+
}
135149
}
136150

137151
if ($IPValidationErrors.Count -gt 0) {
@@ -193,8 +207,20 @@ function Invoke-ExecApiClient {
193207
$FunctionAppName = $env:WEBSITE_SITE_NAME
194208
$AllClients = Get-CIPPAzDataTableEntity @Table -Filter 'Enabled eq true' | Where-Object { ![string]::IsNullOrEmpty($_.RowKey) }
195209
$ClientIds = $AllClients.RowKey
210+
# MCPAllowed can round-trip from table storage as a bool or a string; compare on string form.
211+
$McpClientIds = @($AllClients | Where-Object { "$($_.MCPAllowed)" -eq 'True' } | ForEach-Object { $_.RowKey })
212+
Write-Information "[ExecApiClient] MCP clients resolved for audiences/scope: $($McpClientIds -join ', ')"
196213
try {
197-
Set-CippApiAuth -RGName $RGName -FunctionAppName $FunctionAppName -TenantId $TenantId -ClientIds $ClientIds
214+
Set-CippApiAuth -RGName $RGName -FunctionAppName $FunctionAppName -TenantId $TenantId -ClientIds $ClientIds -McpClientIds $McpClientIds
215+
216+
# Advertise the MCP resource scope via App Service PRM so the Claude connector requests
217+
# a scope that matches the resource app (clears AADSTS9010010). Cleared when no MCP clients.
218+
if ($McpClientIds.Count -gt 0 -and $env:WEBSITE_HOSTNAME) {
219+
$null = Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $RGName -AppSetting @{ 'WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES' = "https://$($env:WEBSITE_HOSTNAME)/user_impersonation" }
220+
} else {
221+
$null = Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $RGName -AppSetting @{} -RemoveKeys @('WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES')
222+
}
223+
198224
$Body = @{ Results = 'API clients saved to Azure' }
199225
Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message 'Saved API clients to Azure' -Sev 'Info'
200226
} catch {

0 commit comments

Comments
 (0)