<# .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-. .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. .PARAMETER Delete Remove the saved tenant credentials from the local settings file (and macOS Keychain if applicable). Does not delete the app registration in Entra ID. .PARAMETER DeleteApp Remove the app registration from the Entra tenant and clean up local credentials. Requires the same Microsoft Graph permissions as initialization. #> [CmdletBinding()] param( [string]$TenantId, [string]$DisplayName = "IntuneManagement-$([Environment]::UserName)", [string]$SettingsFile, [switch]$Force, [switch]$Delete, [switch]$DeleteApp ) $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 } function Remove-LocalAuthSettings { param([string]$TenantId, [string]$AppId) if ($global:JsonSettingsObj) { if ($global:JsonSettingsObj.ContainsKey($TenantId)) { $global:JsonSettingsObj.Remove($TenantId) | Out-Null Write-Host "Removed tenant settings for $TenantId from $SettingsFile" -ForegroundColor Green } if ($global:JsonSettingsObj["TenantId"] -eq $TenantId) { $global:JsonSettingsObj.Remove("TenantId") | Out-Null Write-Host "Removed default TenantId from $SettingsFile" -ForegroundColor Green } $global:JsonSettingsObj | ConvertTo-Json -Depth 30 | Out-File -LiteralPath $global:JSonSettingFile -Force -Encoding utf8 } if ($AppId) { if ($IsMacOS) { $null = security delete-generic-password -a "IntuneManagement" -s "IntuneMgmt-$AppId" 2>$null Write-Host "Removed client secret for AppId $AppId from macOS Keychain" -ForegroundColor Green } else { Write-Host "Client secret was stored in $SettingsFile and has been removed along with the tenant node." -ForegroundColor Green } } else { Write-Warning "No saved credentials found for tenant $TenantId." } } #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 Delete saved credentials if ($Delete) { $appIdToClean = Get-AuthSetting -SubPath $TenantId -Key "GraphAzureAppId" Remove-LocalAuthSettings -TenantId $TenantId -AppId $appIdToClean 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", "Organization.Read.All" -NoWelcome #endregion #region Resolve authenticated user for app naming if (-not $PSBoundParameters.ContainsKey('DisplayName')) { try { $ctx = Get-MgContext -ErrorAction Stop if ($ctx -and $ctx.Account) { $DisplayName = "IntuneManagement-$($ctx.Account)" Write-Host "Using app display name: $DisplayName" -ForegroundColor DarkGray } } catch { } } #endregion #region Cache tenant name try { $org = Get-MgOrganization -ErrorAction Stop if ($org -and $org.DisplayName) { Save-AuthSetting -SubPath $TenantId -Key "TenantName" -Value $org.DisplayName Write-Host "Cached tenant name: $($org.DisplayName)" -ForegroundColor Green } } catch { Write-Warning "Failed to cache tenant name: $($_.Exception.Message)" } #endregion #region Delete app registration and local credentials if ($DeleteApp) { $appIdToClean = Get-AuthSetting -SubPath $TenantId -Key "GraphAzureAppId" if ($appIdToClean) { $appToDelete = Get-MgApplication -Filter "appId eq '$appIdToClean'" -ErrorAction SilentlyContinue | Select-Object -First 1 if ($appToDelete) { Remove-MgApplication -ApplicationId $appToDelete.Id Write-Host "Deleted app registration $($appToDelete.DisplayName) ($appIdToClean) from tenant $TenantId" -ForegroundColor Green } else { Write-Warning "App registration $appIdToClean not found in tenant $TenantId." } } else { Write-Warning "No AppId found in local settings for tenant $TenantId." } Remove-LocalAuthSettings -TenantId $TenantId -AppId $appIdToClean Disconnect-MgGraph | Out-Null return } #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 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