Automate Devices Primary Users

How to Automatically Set Windows Device Primary User in Intune Based on Last Logged-On User

In many Microsoft Intune environments, the Primary User assigned to a Windows device often becomes outdated or misaligned with the last logged-on user. This discrepancy can occur due to device reassignments, hybrid join issues, or manual onboarding workflows. When left unresolved, it leads to inaccurate licensing, unreliable reporting, ineffective security audits, and unnecessary friction for end users.

Accurate primary user assignments are essential for:

  • Compliance auditing
  • License entitlement management
  • Security incident investigation
  • IT support workflows
  • App targeting and Company Portal UX

In this guide, we’ll walk you through automating primary user updates in Intune using a PowerShell script integrated with the Microsoft Graph API and hosted in Azure Automation. You’ll get:

  • A ready-to-use script
  • Step-by-step instructions
  • A secure integration using Azure Key Vault and Managed Identity
  • Visuals to guide you through the configuration process

Why Accurate Primary User Assignment Matters

Assigning the correct Primary User for a Windows device is not just a matter of hygiene—it’s foundational to enterprise compliance and management.

Licensing Compliance

Intune ties certain licenses, like Windows 10/11 Enterprise E3/E5 or Microsoft 365 Apps for enterprise, to a device’s primary user. If the user listed is outdated, you risk:

  • Over-licensing: Allocating resources to inactive or incorrect users.
  • Under-licensing: Active users without appropriate entitlements.

This can snowball into financial inefficiencies and audit risks.

Reporting & Auditing

Inaccurate primary user data undermines:

  • Device ownership reports
  • Usage trend dashboards
  • Security audit trails

Security engineers depend on correct user-device mappings for breach investigations and suspicious behaviour analysis.

Streamlined IT Support

When a helpdesk ticket is raised, the ability to instantly identify the true owner of a device improves response time and resolution efficiency. Misidentified users can lead to support delays or inaccurate troubleshooting.

Enhanced The Primary User Experience (UX)

Intune uses Primary User data to determine:

  • Company Portal visibility
  • App assignment eligibility
  • Self-service device actions

A mismatch can block users from accessing apps or managing their devices.

Strengthened Security Posture

Security teams require accurate device-user context for:

  • Zero Trust enforcement
  • Conditional Access validation
  • Forensic analysis during incidents

Automating primary user updates ensures consistent, real-time identity-to-device linkage.


Prerequisites for Automation

1. Azure AD Application Registration

A non-interactive Azure AD app is required to authenticate with Microsoft Graph securely.

Check out my blog on how to register an application in Azure/Entra ID for Microsoft Graph API access.

Permissions Required:

PermissionPurpose
DeviceManagementManagedDevices.ReadWrite.AllRead/update device and user info
User.Read.AllRead user profiles
Mail.SendOptional – for email reporting

🔒 Security Tip: Use least privilege by granting only the permissions needed.

The registered app API Permissions should be something like this

Update Devices Primary Users

2. Azure Automation Account

This is where the PowerShell script will be hosted, run, and scheduled.

Check out my blog on how to setup Azure automation account

Quick steps:

  • Create an Automation Account
Create Azure Automation Account
  • Enable System Assigned Managed Identity
Azure account assigment

3. Azure Key Vault

Never hardcode secrets in your code! Use Azure Key Vault to store your app’s client secrets securely.

Check out my blog on how to create and configure Azure Key Vault for secure secret management.

Quick steps:

  • Create a Key Vault.
  • Add a secret (e.g., GraphAppSecret).
Create Key Vault Secret
  • Assign the Azure Automation Account’s Managed Identity with Key Vault Secret user permissions to access the secret.
Add policy for Automation Account

👉 Read the full guide here: https://sysadminhub.info/azure-key-vault-setup-tutorial-secure-your-secrets-in-the-cloud/


Understanding the Automation Script

This script, Update Devices Primary Users , is part of an automated solution for managing and auditing user logins across an organisation’s Microsoft 365 environment. It helps lay the foundation for identifying discrepancies in device ownership and updating Intune device primary users if needed. Here’s a breakdown of how it works under each logical phase:

  • Authenticate using Azure AD + Key Vault
  • Fetch Intune-managed Windows devices
  • Identify the last logged-on user
  • Compare with the existing primary user
  • Automatically update Intune if needed
  • Generate a report and email summary

Let’s break it down:

Authentication & Key Vault Integration

To securely access Microsoft Graph and Intune APIs, authentication is established using an Azure AD App Registration and Azure Key Vault for secret management. This ensures credentials like client secrets or certificates aren’t hardcoded in your script.

  • Step 1: Register an Azure AD App in Entra ID with DeviceManagementManagedDevices.ReadWrite.All and User.Read.All API permissions.
  • Step 2: Store the client secret or certificate securely in Azure Key Vault.
  • Step 3: In the Azure Automation Account, create a RunAs or Managed Identity and assign it Key Vault Secrets User and Reader roles.
  • Step 4: Use PowerShell in Azure Automation to retrieve secrets from Key Vault and get an access token for Microsoft Graph.
$MLSecretName = "<your Secret Name>"    # e.g., "MyIntuneSecretsKV"
$VaultName = "<Your Vault Name>"        # Name of the secret storing your Client Secret in Key Vault

try {
    Enable-AzContextAutosave -Scope Process
    Connect-AzAccount -Identity
    Write-Output "Successfully connected with managed identity"

    $ReturnedMLSecret = Get-AzKeyVaultSecret -VaultName $VaultName -Name $MLSecretName -AsPlainText

    $secretParts = $ReturnedMLSecret.Split()
    if ($secretParts.Count -lt 3) {
        throw "Secret format is incorrect. Expected format: 'ClientSecretValue TenantIDValue ClientIDValue'"
    }
    
    $ClientSecret = $secretParts[0]
    $TenantID     = $secretParts[1]
    $clientId     = $secretParts[2]

    Write-Output "Application Client ID is $clientId"
    Write-Output "Tenant ID is $TenantID"
    Write-Output "Client Secret is (hidden for security)"

📘 Reference: Microsoft Learn – Use Key Vault with Azure Automation

Fetch Intune-managed Windows devices

Using the Microsoft Graph API, query all Windows devices currently enrolled in Intune. Focus on devices Windows and Company managed to filter out stale records.

GET https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=operatingSystem eq 'Windows' and managedDeviceOwnerType eq 'company'&`$select=id,deviceName,userDisplayName,userPrincipalName,usersLoggedOn,azureADDeviceId,enrolledByUserPrincipalName,emailAddress

In PowerShell:

$Devicesuri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=operatingSystem eq 'Windows' and managedDeviceOwnerType eq 'company'&`$select=id,deviceName,userDisplayName,userPrincipalName,usersLoggedOn,azureADDeviceId,enrolledByUserPrincipalName,emailAddress"

        $Devices = Invoke-RestMethod -Method GET `
            -Uri $currentUri `
            -Headers $Headers `
            -ContentType 'application/json' `
            -ErrorAction Stop

📘 Reference: List managed devices – Microsoft Graph


Identify the last logged-on user vs the Primary User

The Microsoft Graph managedDevice The entity includes a userPrincipalName and lastLoggedOnUser field, although the latter may sometimes be null.

  • If lastLoggedOnUser is empty, fall back to userPrincipalName.
  • Optionally, use Windows event logs or Azure Log Analytics to determine the most recent interactive logon.

Example field:

"lastLoggedOnUser": "john.doe@domain.com"

📘 Reference: ManagedDevice Resource Type – Microsoft Graph


Compare with the existing primary user

Each Intune-managed device has an assigned primary user, used for user-based targeting and licensing. This is retrieved from:

if ($DMSUri2.usersLoggedOn -and $DMSUri2.usersLoggedOn.Count -gt 0) {
# Get the last entry in the usersLoggedOn array.
$JsonOutput = $DMSUri2.usersLoggedOn | Sort-Object lastLogOnDateTime | ConvertTo-Json -Depth 5

    $lastLoginEntry = $DMSUri2.usersLoggedOn | Sort-Object lastLogOnDateTime | Select-Object -Last 1
    $LastloginUserId = $lastLoginEntry.userId
    $LastloginTime = $lastLoginEntry.lastLogOnDateTime

    # Get detailed user info for the last logged-on user
    Write-Output "Fetching details for last logged on user $($LastloginUserId)..."
    try {
        $UserLoggedonDetails = Invoke-RestMethod -Method GET `
            -Uri "https://graph.microsoft.com/v1.0/users/$LastloginUserId" `
            -Headers $Headers `
            -ContentType 'application/json' `
            -ErrorAction Stop

        $UserLoggedon_Dispalyname = $UserLoggedonDetails.DisplayName
        $UserLoggedon_UserPrincipalName= $UserLoggedonDetails.UserPrincipalName
    }
    catch {
        Write-Output "Error fetching details for last logged on user $($LastloginUserId): $($_.Exception.Message)"
        $UserLoggedon_Dispalyname = "Unknown"
        $UserLoggedon_UserPrincipalName = "Unknown"
        # Set values to empty/unknown if user details cannot be retrieved
    }
} else {
    Write-Output "No last logged on user data for device $($DeviceName)."
    $LastloginUserId = "N/A"
    $LastloginTime = "N/A"
    $UserLoggedon_Dispalyname = "N/A"
    $UserLoggedon_UserPrincipalName = "N/A"
    $MorethanOneLogin = "N/A"
}

Compare the result with the actual last logged-on user.

📘 Reference: How user-device association works in Intune


Automatically update the Intune Primary User if needed

If there’s a mismatch between the last logged-on user and the current primary user, use Microsoft Graph to update the primary user:

  $UpdatePrimaryUseruri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$($DeviceId)')/users/`$ref"
        $UpdatePrimaryUserBody = @{
            "@odata.id" = "https://graph.microsoft.com/beta/users/$LastloginUserId"
        } | ConvertTo-Json

        $currentUpdateHeaders = @{ # Re-use the existing $Headers to avoid re-defining token
            "Authorization" = "Bearer $accessToken"
            "Content-Type"  = "application/json"
        }

⚠️ This operation is only supported in the beta endpoint and should be tested before production use. Microsoft recommends caution as changes could affect app deployment and policy targeting.

📘 Reference: Assign user to device – Microsoft Graph (beta)


Generate a report and email summary

Finally, generate a CSV report summarising:

Device NameCurrent Primary UserLast Logged-On UserAction TakenTimestamp

Store the CSV in a secure location (like Azure Blob or OneDrive via Graph), then use an SMTP or Graph Mail API to send the summary to your team.

Example PowerShell snippet to email:

# Send an email with the CSV attachments
$emailContent = @"
<html>
<head>
<style>
    body { font-family: Arial, sans-serif; margin: 20px; color: #333; }
    table { border-collapse: collapse; width: 100%; margin-top: 20px; margin-bottom: 30px; background-color: #ffffff; box-shadow: 0 0 10px rgba(0,0,0,0.1); border-radius: 8px; overflow: hidden; }
    th, td { border: 1px solid #e0e0e0; padding: 12px 15px; text-align: left; }
    th { background-color: #0078d4; color: #ffffff; font-weight: bold; text-transform: uppercase; }
    tr:nth-child(even) { background-color: #f8f8f8; }
    h1 { color: #0078d4; border-bottom: 2px solid #0078d4; padding-bottom: 10px; margin-bottom: 20px; }
    h2 { color: #333; border-bottom: 1px solid #ddd; padding-bottom: 5px; margin-top: 40px; }
    .summary { background-color: #e6f2fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border: 1px solid #b3d9ff; }
    p { line-height: 1.6; }
</style>
</head>
<body>
<div class="summary">
    <h1>Devices Primary Users Vs Last Logon Users Report - $(Get-Date -Format 'yyyy-MM-dd')</h1>
    <p>Please find attached reports for devices managed by Intune.</p>
</div>

$PrimarVsLogonUsersHtmlBody

$body
</body>
</html>
"@

$messageBody = @{
    message = @{
        subject = $subject
        body = @{
            contentType = 'html'
            content = $emailContent
        }
        toRecipients = $toRecipients

        attachments = @(
            @{
                '@odata.type' = '#microsoft.graph.fileAttachment'
                name = "AllDevicesReport_$(Get-Date -Format 'yyyy-MM-dd').csv"
                contentBytes = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($tempPath))
            },
            @{
                '@odata.type' = '#microsoft.graph.fileAttachment'
                name = "PrimaryUserChangeReport_$(Get-Date -Format 'yyyy-MM-dd').csv"
                contentBytes = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($PrimaryUserChangeReport))
            }
        )
    }
    saveToSentItems = $true
}

Write-Output "Sending email to $toEmail..."
try {
    $emailResponse = Invoke-RestMethod -Method POST `
        -Uri "https://graph.microsoft.com/v1.0/users/$fromEmail/sendMail" `
        -Headers $Headers `
        -Body ($messageBody | ConvertTo-Json -Depth 10) `
        -ContentType 'application/json' `
        -ErrorAction Stop

    Write-Output "Email sent successfully!"
}
catch {
    Write-Output "Email sending failed: $($_.Exception.Message)"
    Write-Output "Please check Mail.Send permission for the sending user/application ($fromEmail)."
}

Cleanup Primary User Change Report

All temporary report files are deleted at the end of the script execution.

# Clean up the temporary files
Write-Output "Cleaning up temporary files..."
try {
    Remove-Item -Path $tempPath -ErrorAction SilentlyContinue
    Remove-Item -Path $PrimaryUserChangeReport -ErrorAction SilentlyContinue
    Write-Output "Temporary files cleaned up."
}
catch {
    Write-Output "Error cleaning up temporary files: $($_.Exception.Message)"
}

Write-Output "Script execution finished."

How to Implement and Run the Script with Azure Automation

  • Download the Update Devices Primary Users from my GitHub
  • Update environment-specific values:
    • $VaultName, $secretnames, $fromEmail, $toEmail
  • Go to your Azure Automation Account and create a runbook
    • Runtime: PowerShell 5.1 or 7
    • Type: PowerShell
    • Name: UpdateIntuneDevicesPrimaryUser
Creating a runbook to automat powrshell script
  • Click Review + Create
  • A code editing page will open, past the code you downloaded from GitHub
Adding script to azure automation
  • Set a schedule:
Link to Schedule
Link a Schedule to your runbook
Set workbook Schedule
  • Review job logs and output:
Review the job Log
  • Review your report:

Conclusion

Automating the update of Intune Primary Users delivers tangible value: accurate licensing, actionable reporting, streamlined support, and improved security posture. By integrating PowerShell, Microsoft Graph API, and Azure Automation with secure practices like Key Vault and Managed Identity, this solution enhances your endpoint management strategy without manual overhead.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

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