From 70679cba48efd9457034cb668d12f565e05f84d9 Mon Sep 17 00:00:00 2001 From: Tomas Kracmar Date: Thu, 16 Apr 2026 15:40:33 +0200 Subject: [PATCH] v4.0.1: per-user app naming, auth deletion, TUI onboarding flow, PIM docs --- CHANGELOG_macOS_IntuneToolkit.md | 20 ++++ README.md | 11 +- Scripts/Initialize-IntuneAuth.ps1 | 168 +++++++++++++++++++++++++----- Scripts/Start-IntuneToolkit.ps1 | 26 +++++ 4 files changed, 200 insertions(+), 25 deletions(-) diff --git a/CHANGELOG_macOS_IntuneToolkit.md b/CHANGELOG_macOS_IntuneToolkit.md index 1e651e4..1cd573f 100644 --- a/CHANGELOG_macOS_IntuneToolkit.md +++ b/CHANGELOG_macOS_IntuneToolkit.md @@ -1,5 +1,25 @@ # macOS Intune Toolkit Changelog +## 2026-04-16 — v4.0.1 — Accountability, PIM & Auth Management + +### Modified +- **`Scripts/Initialize-IntuneAuth.ps1`** + - App registrations are now named after the **authenticated Entra user** (e.g., `IntuneManagement-tomas.kracmar@cqre.net`) instead of the local OS username. This improves audit-log traceability when multiple admins use the toolkit against the same tenant. + - Added `-Delete` switch to remove local tenant credentials (`Settings.json` + macOS Keychain) without touching the Entra app registration. + - Added `-DeleteApp` switch to delete both the **Entra app registration** and local credentials. + - Onboarding now automatically caches the tenant display name after auth setup, so the TUI shows friendly names immediately. + - Added `Organization.Read.All` to the `Connect-MgGraph` scopes to support tenant name caching. + +- **`Scripts/Start-IntuneToolkit.ps1`** + - Added menu items **14** (delete local auth) and **15** (delete auth + app registration) to the TUI. + - Selecting **"[+ Onboard new tenant]"** now runs the auth initializer immediately and restarts the launcher, instead of dropping into the main menu for an unconfigured tenant. + - The TUI now exits cleanly after deleting tenant auth. + +- **`README.md`** + - Added **Accountability & PIM caveats** section explaining the trade-offs of app-only auth versus delegated auth, and how app naming affects audit logs. + +--- + ## 2026-04-13 — API Permissions Sync for `Initialize-IntuneAuth.ps1` ### Modified diff --git a/README.md b/README.md index 17396e3..f397071 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Cross-platform, headless Intune policy export/import with PowerShell. -**Current version:** `4.0.0` — see [`CHANGELOG_macOS_IntuneToolkit.md`](CHANGELOG_macOS_IntuneToolkit.md) for recent changes. +**Current version:** `4.0.1` — see [`CHANGELOG_macOS_IntuneToolkit.md`](CHANGELOG_macOS_IntuneToolkit.md) for recent changes. This repository is now CLI-first. The old WPF application surface has been removed from the repo. The supported workflow is: @@ -154,3 +154,12 @@ pwsh ./Start-HeadlessIntune.ps1 ` * Browser auth uses the system browser and a loopback redirect. * If you omit `-AppId` with `-AuthMode Browser`, the CLI defaults to the Microsoft Graph PowerShell public client app id `14d82eec-204b-4c2f-b7e8-296a70dab67e`. * If your own app registration does not allow loopback redirects, pass `-AppId` and `-RedirectUri "http://localhost"` and configure the same redirect URI in Entra ID. + +## Accountability & PIM caveats + +By default `Initialize-IntuneAuth.ps1` creates an **app-only** registration. Every Graph call is authenticated as the service principal, not as an individual user. + +* **Audit logs** show the app's display name (e.g., `IntuneManagement-tomas.kracmar@cqre.net`), not the admin's UPN. The initializer now automatically names the app after the **authenticated Entra user** to improve traceability. +* **PIM is not enforced** for app-only secrets. The service principal has standing permissions, so write operations can occur outside an elevated PIM window. +* If you need strict PIM compliance, use **delegated authentication** (`-AuthMode Browser` or `-AuthMode DeviceCode`) so calls are made in the signed-in user's context. Note that `DeviceCode` may be blocked by Conditional Access policies. +* To fully remove a tenant's local credentials **and** the Entra app registration, use menu item **15** in the TUI or run `./Scripts/Initialize-IntuneAuth.ps1 -TenantId "" -DeleteApp`. diff --git a/Scripts/Initialize-IntuneAuth.ps1 b/Scripts/Initialize-IntuneAuth.ps1 index 93c6214..e6d5cb8 100644 --- a/Scripts/Initialize-IntuneAuth.ps1 +++ b/Scripts/Initialize-IntuneAuth.ps1 @@ -17,7 +17,7 @@ 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. +The display name for the app registration. Default: IntuneManagement-. .PARAMETER SettingsFile Path to the JSON settings file. If omitted, defaults to the macOS_IntuneManagement @@ -25,16 +25,28 @@ settings folder (~/Library/Application Support/macOS_IntuneManagement/Settings.j .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-Headless", + [string]$DisplayName = "IntuneManagement-$([Environment]::UserName)", [string]$SettingsFile, - [switch]$Force + [switch]$Force, + + [switch]$Delete, + + [switch]$DeleteApp ) $ErrorActionPreference = "Stop" @@ -67,6 +79,45 @@ 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 @@ -85,6 +136,96 @@ if (-not $TenantId) } #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) @@ -116,27 +257,6 @@ if ($existingAppId -and -not $Force) } #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) diff --git a/Scripts/Start-IntuneToolkit.ps1 b/Scripts/Start-IntuneToolkit.ps1 index 30d6a52..61d1005 100644 --- a/Scripts/Start-IntuneToolkit.ps1 +++ b/Scripts/Start-IntuneToolkit.ps1 @@ -261,6 +261,12 @@ if(-not $TenantId) Write-Host "No tenant ID provided. Exiting." -ForegroundColor Yellow exit 0 } + $initPath = Join-Path $projectRoot "Scripts/Initialize-IntuneAuth.ps1" + & $initPath -TenantId $TenantId + Write-Host "`nOnboarding complete. Restarting launcher..." -ForegroundColor Green + Start-Sleep -Seconds 1 + & $PSCommandPath + exit 0 } else { @@ -300,6 +306,8 @@ $commonParams = @{ } $menuItems = @( + "15. Delete tenant auth and app registration" + "14. Delete local tenant auth only" "13. Refresh tenant names" "12. Initialize auth (one-time setup)" "11. Deploy baseline (dry-run / WhatIf)" @@ -360,6 +368,8 @@ while($true) 11 { $script = "Scripts/Deploy-IntuneBaseline.ps1"; $commonParams.WhatIf = $true } 12 { $script = "Scripts/Initialize-IntuneAuth.ps1" } 13 { $script = $null } + 14 { $script = "Scripts/Initialize-IntuneAuth.ps1" } + 15 { $script = "Scripts/Initialize-IntuneAuth.ps1" } default { continue } } @@ -424,9 +434,25 @@ while($true) @("AppId","Secret","Certificate","AuthMode","RedirectUri","Interactive","Mode","WhatIf") | ForEach-Object { $launchParams.Remove($_) } } + if($choiceNumber -eq 14) + { + $launchParams.Delete = $true + } + + if($choiceNumber -eq 15) + { + $launchParams.DeleteApp = $true + } + # Execute in same process so TUI flows naturally & $scriptPath @launchParams + if($choiceNumber -eq 14 -or $choiceNumber -eq 15) + { + Write-Host "`nTenant auth deleted. Exiting." -ForegroundColor Yellow + exit 0 + } + Write-Host "`nPress any key to return to the menu..." -ForegroundColor DarkGray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") }