PowerShell 7 Intune Primary User Management: Complete Automation Guide 2025
Complete PowerShell 7 Guide: Intune Primary User Management
Understanding the Primary User Concept
The Primary User property in Intune serves several critical functions :
| Function | Description |
|---|---|
| License Mapping | Associates a licensed Intune user to the device |
| Company Portal | Displays the device in the user’s Company Portal app |
| Device Management | Shows device in the user’s Device Management portal |
| Support Context | Makes it easier for admins to identify device ownership |
Consequences of incorrect primary user assignment:
- Company Portal shows limited functionality and missing apps
- Warning message: “This device is already assigned to someone in your organization”
- Devices without primary users are treated as shared devices
Prerequisites and Environment Setup
Required Modules for PowerShell 7
PowerShell 7 is strongly recommended over Windows PowerShell 5.1. The Microsoft Graph PowerShell SDK has known compatibility issues with Windows PowerShell that remain unresolved .
# Install required modules (run as Administrator)
Install-Module -Name Microsoft.Graph.Authentication -Force -Scope AllUsers
Install-Module -Name Microsoft.Graph.DeviceManagement -Force -Scope AllUsers
Install-Module -Name Microsoft.Graph.Users -Force -Scope AllUsers
# For legacy approaches (not recommended for new projects):
# Install-Module -Name Microsoft.Graph.Intune -Force
Authentication and Permissions
Connect with appropriate scopes based on your operation:
# Read-only operations
Connect-MgGraph -Scopes "DeviceManagementManagedDevices.Read.All", "User.Read.All"
# Read-write operations (required for setting primary user)
Connect-MgGraph -Scopes "DeviceManagementManagedDevices.ReadWrite.All", "User.Read.All"
Core Operations: The Complete Technical Reference
1. Retrieving the Current Primary User
Method A: Using Invoke-MgGraphRequest (Recommended for PowerShell 7)
function Get-IntuneDevicePrimaryUser {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$DeviceId
)
$uri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/$DeviceId/users"
try {
$response = Invoke-MgGraphRequest -Method GET -Uri $uri
return $response.value | Select-Object -First 1
}
catch {
Write-Error "Failed to retrieve primary user: $_"
return $null
}
}
# Usage
$deviceId = "your-intune-device-id"
$primaryUser = Get-IntuneDevicePrimaryUser -DeviceId $deviceId
if ($primaryUser) {
Write-Host "Primary User: $($primaryUser.displayName) ($($primaryUser.userPrincipalName))"
}
Method B: Using the Beta Endpoint (More Detailed Information)
$deviceId = "your-device-id"
$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$deviceId/users"
$primaryUser = Invoke-MgGraphRequest -Method GET -Uri $uri
2. Setting the Primary User
This is the critical operation that requires precise JSON formatting. The Graph API uses an OData reference structure :
function Set-IntuneDevicePrimaryUser {
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[Parameter(Mandatory = $true)]
[string]$DeviceId,
[Parameter(Mandatory = $true)]
[string]$UserId,
[switch]$Force
)
# Validate inputs
if ([string]::IsNullOrWhiteSpace($DeviceId)) {
throw "DeviceId cannot be null or empty"
}
if ([string]::IsNullOrWhiteSpace($UserId)) {
throw "UserId cannot be null or empty"
}
$uri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/$DeviceId/users/`$ref"
# Construct the OData reference payload
$body = @{
"@odata.id" = "https://graph.microsoft.com/v1.0/users/$UserId"
} | ConvertTo-Json -Depth 3
Write-Verbose "URI: $uri"
Write-Verbose "Payload: $body"
if ($PSCmdlet.ShouldProcess("Device $DeviceId", "Set primary user to $UserId")) {
try {
Invoke-MgGraphRequest -Method POST -Uri $uri -Body $body -ContentType "application/json"
Write-Host "โ Successfully set primary user for device $DeviceId" -ForegroundColor Green
return $true
}
catch {
# Check if it's already set to this user (409 Conflict)
if ($_.Exception.Response.StatusCode -eq 409) {
Write-Warning "Primary user may already be set to this user"
}
Write-Error "Failed to set primary user: $_"
return $false
}
}
}
3. Removing the Primary User
function Remove-IntuneDevicePrimaryUser {
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[Parameter(Mandatory = $true)]
[string]$DeviceId
)
$uri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/$DeviceId/users/`$ref"
if ($PSCmdlet.ShouldProcess("Device $DeviceId", "Remove primary user")) {
try {
Invoke-MgGraphRequest -Method DELETE -Uri $uri
Write-Host "โ Successfully removed primary user from device $DeviceId" -ForegroundColor Green
}
catch {
Write-Error "Failed to remove primary user: $_"
}
}
}
Advanced Automation Scenarios
Scenario 1: Bulk Update Based on Last Logged-On User
This is the most common automation needโcorrecting primary users after imaging or when devices were enrolled with provisioning accounts :
function Update-PrimaryUserToLastLogon {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string[]]$DeviceNames,
[int]$LookbackDays = 30
)
$results = @()
foreach ($deviceName in $DeviceNames) {
Write-Host "`nProcessing $deviceName..." -ForegroundColor Cyan
# Get device details
$filter = "deviceName eq '$deviceName' and operatingSystem eq 'Windows'"
$device = Get-MgDeviceManagementManagedDevice -Filter $filter -Top 1
if (-not $device) {
Write-Warning "Device $deviceName not found in Intune"
$results += [PSCustomObject]@{
DeviceName = $deviceName
Status = "NotFound"
PreviousUser = $null
NewUser = $null
}
continue
}
$deviceId = $device.Id
# Get current primary user
$currentPrimaryUri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$deviceId/users"
$currentPrimary = (Invoke-MgGraphRequest -Method GET -Uri $currentPrimaryUri).value | Select-Object -First 1
# Get last logged on users (sorted by time)
$lastLogons = $device.UsersLoggedOn | Sort-Object -Property lastLogOnDateTime -Descending
if (-not $lastLogons) {
Write-Warning "No logon history found for $deviceName"
continue
}
$mostRecentLogon = $lastLogons | Select-Object -First 1
$lastLogonUserId = $mostRecentLogon.userId
$lastLogonTime = $mostRecentLogon.lastLogOnDateTime
# Check if logon is within lookback period
$cutoffDate = (Get-Date).AddDays(-$LookbackDays)
if ([datetime]$lastLogonTime -lt $cutoffDate) {
Write-Warning "Last logon for $deviceName was at $lastLogonTime (older than $LookbackDays days)"
}
# Get user details
$lastLogonUser = Get-MgUser -UserId $lastLogonUserId -Property "displayName,userPrincipalName,id"
# Check if update is needed
if ($currentPrimary -and $currentPrimary.id -eq $lastLogonUserId) {
Write-Host " Primary user already matches last logon user: $($lastLogonUser.displayName)" -ForegroundColor Yellow
continue
}
# Perform the update
$success = Set-IntuneDevicePrimaryUser -DeviceId $deviceId -UserId $lastLogonUserId
$results += [PSCustomObject]@{
DeviceName = $deviceName
DeviceId = $deviceId
Status = if ($success) { "Updated" } else { "Failed" }
PreviousUser = $currentPrimary.userPrincipalName
NewUser = $lastLogonUser.userPrincipalName
LastLogonTime = $lastLogonTime
}
}
return $results
}
# Execute bulk update
$devices = @("DESKTOP-001", "DESKTOP-002", "LAPTOP-003")
$report = Update-PrimaryUserToLastLogon -DeviceNames $devices -LookbackDays 30
$report | Format-Table -AutoSize
Scenario 2: CSV-Based Bulk Assignment
For controlled migrations or new device provisioning :
function Import-PrimaryUserAssignments {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$CsvPath,
[switch]$WhatIf
)
# Expected CSV columns: DeviceName, UserPrincipalName (or UserId)
$assignments = Import-Csv -Path $CsvPath
$report = foreach ($row in $assignments) {
$deviceName = $row.DeviceName
$userPrincipalName = $row.UserPrincipalName
Write-Host "Processing: $deviceName -> $userPrincipalName"
# Resolve user
$user = Get-MgUser -Filter "userPrincipalName eq '$userPrincipalName'" -Property "id,displayName"
if (-not $user) {
Write-Warning "User $userPrincipalName not found"
[PSCustomObject]@{
DeviceName = $deviceName
UserPrincipalName = $userPrincipalName
Status = "UserNotFound"
}
continue
}
# Resolve device
$device = Get-MgDeviceManagementManagedDevice -Filter "deviceName eq '$deviceName'" -Top 1
if (-not $device) {
Write-Warning "Device $deviceName not found"
[PSCustomObject]@{
DeviceName = $deviceName
UserPrincipalName = $userPrincipalName
Status = "DeviceNotFound"
}
continue
}
# Check current state
$currentUri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/$($device.Id)/users"
$current = (Invoke-MgGraphRequest -Uri $currentUri -Method GET).value | Select-Object -First 1
if ($current.id -eq $user.Id) {
Write-Host " Already correctly assigned" -ForegroundColor Green
[PSCustomObject]@{
DeviceName = $deviceName
UserPrincipalName = $userPrincipalName
Status = "AlreadyCorrect"
}
}
else {
if (-not $WhatIf) {
$success = Set-IntuneDevicePrimaryUser -DeviceId $device.Id -UserId $user.Id
[PSCustomObject]@{
DeviceName = $deviceName
UserPrincipalName = $userPrincipalName
Status = if ($success) { "Updated" } else { "Failed" }
PreviousUser = $current.userPrincipalName
}
}
else {
Write-Host " [WHATIF] Would update primary user" -ForegroundColor Cyan
[PSCustomObject]@{
DeviceName = $deviceName
UserPrincipalName = $userPrincipalName
Status = "WhatIf"
}
}
}
}
return $report
}
Scenario 3: Azure Automation Runbook (Scheduled Execution)
For enterprise environments requiring automated daily synchronization :
# Requires: Microsoft.Graph.Authentication module in Azure Automation
# Recommended Runtime: PowerShell 5.1 in Azure Automation (for module compatibility)
param(
[Parameter(Mandatory = $false)]
[ValidateSet("Test", "Prod")]
[string]$ExecutionMode = "Test"
)
# Connect using Managed Identity (System-assigned)
Connect-MgGraph -Identity
# Configuration
$LogAnalyticsWorkspaceId = $env:LogAnalyticsWorkspaceId
$LogAnalyticsKey = $env:LogAnalyticsKey
# Get all Windows devices
$devices = Get-MgDeviceManagementManagedDevice -Filter "operatingSystem eq 'Windows'" -All
$operations = @()
foreach ($device in $devices) {
# Skip devices without logon history
if (-not $device.UsersLoggedOn) { continue }
$lastLogon = $device.UsersLoggedOn |
Sort-Object lastLogOnDateTime -Descending |
Select-Object -First 1
# Get current primary user
$uri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/$($device.Id)/users"
$currentPrimary = (Invoke-MgGraphRequest -Uri $uri -Method GET).value |
Select-Object -First 1
# Determine if update needed
if ($currentPrimary.id -ne $lastLogon.userId) {
$operation = @{
DeviceName = $device.DeviceName
DeviceId = $device.Id
CurrentPrimary = $currentPrimary.userPrincipalName
ProposedPrimary = $lastLogon.userId
LastLogonTime = $lastLogon.lastLogOnDateTime
Action = if ($ExecutionMode -eq "Prod") { "Update" } else { "WouldUpdate" }
}
if ($ExecutionMode -eq "Prod") {
$body = @{
"@odata.id" = "https://graph.microsoft.com/v1.0/users/$($lastLogon.userId)"
} | ConvertTo-Json
try {
Invoke-MgGraphRequest -Method POST -Uri "$uri/`$ref" -Body $body
$operation.Result = "Success"
}
catch {
$operation.Result = "Failed: $_"
}
}
$operations += $operation
}
}
# Output summary
Write-Output "Processed $($devices.Count) devices"
Write-Output "Changes made/pending: $($operations.Count)"
if ($operations.Count -gt 0) {
$operations | ConvertTo-Json -Depth 3
}
Error Handling and Troubleshooting
Common Issues and Solutions
| Issue | Cause | Solution |
|---|---|---|
404 Not Found | Device ID doesn’t exist | Verify device is enrolled and ID is correct |
400 Bad Request | Malformed JSON payload | Ensure @odata.id format is exact |
403 Forbidden | Insufficient permissions | Verify DeviceManagementManagedDevices.ReadWrite.All consent |
409 Conflict | User already primary | Check current state before updating |
| Module import fails in PS 5.1 | Compatibility issue | Use PowerShell 7 |
Diagnostic Script
function Test-IntunePrimaryUserOperations {
[CmdletBinding()]
param (
[string]$TestDeviceId,
[string]$TestUserId
)
Write-Host "=== Intune Primary User Diagnostics ===" -ForegroundColor Cyan
# Test 1: Authentication
Write-Host "`n1. Testing Authentication..." -ForegroundColor Yellow
$context = Get-MgContext
if (-not $context) {
Write-Error "Not authenticated to Microsoft Graph"
return
}
Write-Host " Authenticated as: $($context.Account)" -ForegroundColor Green
Write-Host " Scopes: $($context.Scopes -join ', ')"
# Test 2: Device Access
if ($TestDeviceId) {
Write-Host "`n2. Testing Device Access..." -ForegroundColor Yellow
try {
$device = Get-MgDeviceManagementManagedDevice -ManagedDeviceId $TestDeviceId
Write-Host " Device found: $($device.DeviceName)" -ForegroundColor Green
Write-Host " OS: $($device.OperatingSystem)"
Write-Host " Compliance: $($device.ComplianceState)"
}
catch {
Write-Error " Cannot access device: $_"
}
}
# Test 3: User Access
if ($TestUserId) {
Write-Host "`n3. Testing User Access..." -ForegroundColor Yellow
try {
$user = Get-MgUser -UserId $TestUserId -Property "id,displayName,userPrincipalName"
Write-Host " User found: $($user.DisplayName)" -ForegroundColor Green
}
catch {
Write-Error " Cannot access user: $_"
}
}
# Test 4: Primary User Read
if ($TestDeviceId) {
Write-Host "`n4. Testing Primary User Read..." -ForegroundColor Yellow
try {
$uri = "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/$TestDeviceId/users"
$response = Invoke-MgGraphRequest -Uri $uri -Method GET
if ($response.value) {
Write-Host " Primary user: $($response.value[0].displayName)" -ForegroundColor Green
}
else {
Write-Host " No primary user set" -ForegroundColor Yellow
}
}
catch {
Write-Error " Cannot read primary user: $_"
}
}
Write-Host "`n=== Diagnostics Complete ===" -ForegroundColor Cyan
}
Best Practices Summary
- Always use PowerShell 7 for Microsoft Graph SDK operations to avoid module compatibility issues
- Use
Invoke-MgGraphRequestfor primary user operations rather than older Intune-specific modules for future-proofing - Implement
-WhatIfsupport usingSupportsShouldProcessfor all destructive operations - Validate before updatingโalways check if the current primary user matches your target
- Log everythingโmaintain audit trails of all primary user changes
- Handle shared devicesโdevices with no primary user are valid shared device scenarios; don’t auto-assign without criteria
- Respect the 30-day lookbackโwhen using last-logon logic, ensure the logon is recent enough to be relevant
