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:
Permission | Purpose |
---|---|
DeviceManagementManagedDevices.ReadWrite.All | Read/update device and user info |
User.Read.All | Read user profiles |
Mail.Send | Optional – for email reporting |
🔒 Security Tip: Use least privilege by granting only the permissions needed.
The registered app API Permissions should be something like this

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

- Enable System Assigned Managed Identity

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
).

- Assign the Azure Automation Account’s Managed Identity with
Key Vault Secret user
permissions to access the secret.

👉 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
andUser.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 touserPrincipalName
. - 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 Name | Current Primary User | Last Logged-On User | Action Taken | Timestamp |
---|
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

- Click Review + Create
- A code editing page will open, past the code you downloaded from GitHub

- Set a schedule:



- Review job logs and output:

- 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.