454 lines
14 KiB
PowerShell
454 lines
14 KiB
PowerShell
<#
|
|
.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-<current user name>.
|
|
|
|
.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 : <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
|