|

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 :

FunctionDescription
License MappingAssociates a licensed Intune user to the device
Company PortalDisplays the device in the user’s Company Portal app
Device ManagementShows device in the user’s Device Management portal
Support ContextMakes 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

IssueCauseSolution
404 Not FoundDevice ID doesn’t existVerify device is enrolled and ID is correct
400 Bad RequestMalformed JSON payloadEnsure @odata.id format is exact
403 ForbiddenInsufficient permissionsVerify DeviceManagementManagedDevices.ReadWrite.All consent
409 ConflictUser already primaryCheck current state before updating
Module import fails in PS 5.1Compatibility issueUse 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

  1. Always use PowerShell 7 for Microsoft Graph SDK operations to avoid module compatibility issues
  2. Use Invoke-MgGraphRequest for primary user operations rather than older Intune-specific modules for future-proofing
  3. Implement -WhatIf support using SupportsShouldProcess for all destructive operations
  4. Validate before updatingโ€”always check if the current primary user matches your target
  5. Log everythingโ€”maintain audit trails of all primary user changes
  6. Handle shared devicesโ€”devices with no primary user are valid shared device scenarios; don’t auto-assign without criteria
  7. Respect the 30-day lookbackโ€”when using last-logon logic, ensure the logon is recent enough to be relevant

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *