<# .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 : " } 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 ' 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