feat(auth): sync full Graph permission set and patch existing apps

- Unified required Microsoft Graph app roles in Initialize-IntuneAuth.ps1
- Added permission patching for existing app registrations
- Logs the change and operations for audit
This commit is contained in:
2026-04-14 12:15:14 +02:00
parent 9dace83cff
commit 87b7af25a7
3 changed files with 446 additions and 0 deletions

View File

@@ -0,0 +1,333 @@
<#
.SYNOPSIS
One-time setup helper for IntuneManagement headless authentication.
.DESCRIPTION
Creates a Microsoft Entra app registration (or reuses an existing one),
adds required Microsoft Graph permissions, creates a client secret, and
stores credentials securely:
- TenantId and AppId are saved to the JSON settings file.
- Client secret is saved to the macOS Keychain (default on macOS).
Requires: Microsoft.Graph.Authentication, Microsoft.Graph.Applications
Install if missing: Install-Module Microsoft.Graph -Scope CurrentUser
.PARAMETER TenantId
The Microsoft Entra tenant ID (GUID). If omitted, the script reads from
existing settings or prompts interactively.
.PARAMETER DisplayName
The display name for the app registration. Default: IntuneManagement-Headless.
.PARAMETER SettingsFile
Path to the JSON settings file. If omitted, defaults to the macOS_IntuneManagement
settings folder (~/Library/Application Support/macOS_IntuneManagement/Settings.json).
.PARAMETER Force
Recreate the app registration and secret even if existing credentials are found.
#>
[CmdletBinding()]
param(
[string]$TenantId,
[string]$DisplayName = "IntuneManagement-Headless",
[string]$SettingsFile,
[switch]$Force
)
$ErrorActionPreference = "Stop"
#region Helper: settings file
$coreModule = Join-Path (Split-Path -Parent $PSScriptRoot) "Core.psm1"
if (-not (Test-Path $coreModule))
{
throw "Could not find Core.psm1 at $coreModule"
}
Import-Module $coreModule -Force -Global
if (-not $SettingsFile)
{
$dataFolder = Get-CloudApiDataFolder
$SettingsFile = Join-Path $dataFolder "Settings.json"
}
$global:JSonSettingFile = $SettingsFile
Initialize-JsonSettings
function Save-AuthSetting
{
param($Key, $Value, [string]$SubPath = "")
Save-Setting -SubPath $SubPath -Key $Key -Value $Value
}
function Get-AuthSetting
{
param($Key, [string]$SubPath = "", $DefaultValue = $null)
Get-Setting -SubPath $SubPath -Key $Key -DefaultValue $DefaultValue
}
#endregion
#region Determine TenantId
if (-not $TenantId)
{
$TenantId = Get-AuthSetting -Key "TenantId"
if (-not $TenantId)
{
$TenantId = Read-Host "Enter your Microsoft Entra Tenant ID (GUID)"
}
}
if (-not $TenantId)
{
throw "TenantId is required."
}
#endregion
#region Check for existing credentials
$existingAppId = Get-AuthSetting -SubPath $TenantId -Key "GraphAzureAppId"
if ($existingAppId -and -not $Force)
{
$hasSecret = $false
if ($IsMacOS)
{
try
{
$keychainSecret = security find-generic-password -a "IntuneManagement" -s "IntuneMgmt-$existingAppId" -w 2>$null
if ($keychainSecret) { $hasSecret = $true }
}
catch { }
}
else
{
$plainSecret = Get-AuthSetting -SubPath $TenantId -Key "GraphAzureAppSecret"
if ($plainSecret) { $hasSecret = $true }
}
if ($hasSecret)
{
Write-Host ""
Write-Host "Existing credentials already configured for tenant $TenantId (AppId: $existingAppId)." -ForegroundColor Green
Write-Host "Use -Force to recreate the app registration and secret." -ForegroundColor Yellow
Write-Host ""
return
}
}
#endregion
#region Microsoft Graph modules
$requiredModules = @("Microsoft.Graph.Authentication", "Microsoft.Graph.Applications")
foreach ($mod in $requiredModules)
{
if (-not (Get-Module $mod -ListAvailable))
{
throw "Module '$mod' is not installed. Run: Install-Module Microsoft.Graph -Scope CurrentUser"
}
}
Import-Module Microsoft.Graph.Authentication -Force
Import-Module Microsoft.Graph.Applications -Force
#endregion
#region Connect to Graph
Write-Host ""
Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan
Write-Host "A browser window will open for authentication." -ForegroundColor Cyan
Connect-MgGraph -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" -NoWelcome
#endregion
#region App registration
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
if (-not $graphSp)
{
throw "Could not retrieve Microsoft Graph service principal."
}
$requiredRoles = @(
"DeviceManagementApps.ReadWrite.All",
"DeviceManagementConfiguration.ReadWrite.All",
"DeviceManagementManagedDevices.ReadWrite.All",
"DeviceManagementScripts.ReadWrite.All",
"DeviceManagementServiceConfig.ReadWrite.All",
"DeviceManagementRBAC.ReadWrite.All",
"Group.ReadWrite.All",
"Directory.Read.All",
"User.Read.All",
"Organization.Read.All",
"Policy.ReadWrite.ConditionalAccess",
"Agreement.ReadWrite.All",
"CloudPC.ReadWrite.All",
"Application.Read.All"
)
$resourceAccess = @()
foreach ($roleName in $requiredRoles)
{
$appRole = $graphSp.AppRoles | Where-Object { $_.Value -eq $roleName } | Select-Object -First 1
if (-not $appRole)
{
Write-Warning "Could not find app role: $roleName"
continue
}
$resourceAccess += @{ id = $appRole.Id; type = "Role" }
}
$app = $null
$updatedPermissions = $false
if (-not $Force)
{
$existingApps = Get-MgApplication -Filter "displayName eq '$DisplayName'" -All
if ($existingApps)
{
$app = $existingApps | Select-Object -First 1
Write-Host "Reusing existing app registration: $($app.DisplayName) ($($app.AppId))" -ForegroundColor Yellow
# Check for missing permissions and patch if needed
$existingRra = $app.RequiredResourceAccess | Where-Object { $_.resourceAppId -eq "00000003-0000-0000-c000-000000000000" }
$existingIds = @()
if($existingRra -and $existingRra.resourceAccess)
{
$existingIds = $existingRra.resourceAccess | Select-Object -ExpandProperty id
}
$missingAccess = $resourceAccess | Where-Object { $_.id -notin $existingIds }
if($missingAccess)
{
Write-Host "Adding missing Graph API permissions to existing app..." -ForegroundColor Cyan
$newRra = @(@{
resourceAppId = "00000003-0000-0000-c000-000000000000"
resourceAccess = @($existingIds | ForEach-Object { @{ id = $_; type = "Role" } }) + $missingAccess
})
Update-MgApplication -ApplicationId $app.Id -RequiredResourceAccess $newRra
$updatedPermissions = $true
# Refresh app object
$app = Get-MgApplication -ApplicationId $app.Id
}
}
}
if (-not $app)
{
Write-Host "Creating new app registration: $DisplayName" -ForegroundColor Cyan
$appParams = @{
DisplayName = $DisplayName
SignInAudience = "AzureADMyOrg"
RequiredResourceAccess = @(@{
resourceAppId = "00000003-0000-0000-c000-000000000000"
resourceAccess = $resourceAccess
})
}
$app = New-MgApplication @appParams
}
#endregion
#region Service Principal & Admin Consent
$sp = Get-MgServicePrincipal -Filter "appId eq '$($app.AppId)'" -ErrorAction SilentlyContinue
if (-not $sp)
{
Write-Host "Creating service principal for the app..." -ForegroundColor Cyan
$sp = New-MgServicePrincipal -AppId $app.AppId
}
$consentGranted = $false
if ($sp)
{
Write-Host "Granting admin consent for Microsoft Graph permissions..." -ForegroundColor Cyan
$requiredAppRoles = $app.RequiredResourceAccess[0].resourceAccess
foreach ($ra in $requiredAppRoles)
{
$existing = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $sp.Id |
Where-Object { $_.AppRoleId -eq $ra.id }
if (-not $existing)
{
try
{
New-MgServicePrincipalAppRoleAssignment `
-ServicePrincipalId $sp.Id `
-PrincipalId $sp.Id `
-ResourceId $graphSp.Id `
-AppRoleId $ra.id | Out-Null
$consentGranted = $true
}
catch
{
Write-Warning "Failed to grant consent for role $($ra.id): $($_.Exception.Message)"
}
}
else
{
$consentGranted = $true
}
}
}
#endregion
#region Client Secret
Write-Host "Creating client secret..." -ForegroundColor Cyan
$passwordCred = @{
displayName = "IntuneManagementSecret"
endDateTime = (Get-Date).AddYears(1)
}
$secret = Add-MgApplicationPassword -ApplicationId $app.Id -PasswordCredential $passwordCred
#endregion
#region Save settings
Write-Host "Saving settings to $SettingsFile ..." -ForegroundColor Cyan
Save-AuthSetting -SubPath $TenantId -Key "GraphAzureAppId" -Value $app.AppId
Save-AuthSetting -SubPath $TenantId -Key "GraphAzureAppLogin" -Value $true
Save-AuthSetting -Key "TenantId" -Value $TenantId
if ($IsMacOS)
{
Write-Host "Storing client secret in macOS Keychain..." -ForegroundColor Cyan
$null = security add-generic-password -a "IntuneManagement" -s "IntuneMgmt-$($app.AppId)" -w "$($secret.SecretText)" -U 2>$null
}
else
{
Write-Warning "Not running on macOS. Storing client secret in the settings file (less secure)."
Save-AuthSetting -SubPath $TenantId -Key "GraphAzureAppSecret" -Value $secret.SecretText
}
#endregion
#region Summary
Write-Host ""
Write-Host "=============================================================" -ForegroundColor Green
Write-Host "Authentication setup complete!" -ForegroundColor Green
Write-Host "=============================================================" -ForegroundColor Green
Write-Host "TenantId : $TenantId"
Write-Host "AppId : $($app.AppId)"
Write-Host "Settings : $SettingsFile"
if ($IsMacOS)
{
Write-Host "Secret : <stored in macOS Keychain>"
}
else
{
Write-Host "Secret : $($secret.SecretText)"
}
Write-Host "=============================================================" -ForegroundColor Green
if (-not $consentGranted)
{
Write-Host "IMPORTANT: Admin consent could not be granted automatically." -ForegroundColor Yellow
Write-Host " Go to the Entra portal > API Permissions and click" -ForegroundColor Yellow
Write-Host " 'Grant admin consent for <tenant>' before using" -ForegroundColor Yellow
Write-Host " the app for Export or Import." -ForegroundColor Yellow
Write-Host "=============================================================" -ForegroundColor Green
}
else
{
Write-Host "Admin consent granted successfully." -ForegroundColor Green
Write-Host "=============================================================" -ForegroundColor Green
}
Write-Host ""
Write-Host "You can now run exports without specifying -AppId or -Secret:" -ForegroundColor Cyan
Write-Host " ./Scripts/Export-Policies.ps1 -TenantId `"$TenantId`" -ExportPath `"/tmp/intune-export`" -IncludeAssignments" -ForegroundColor Cyan
Write-Host ""
Disconnect-MgGraph | Out-Null
#endregion