Introduction
When enrolling devices into Microsoft Intune using the Company Portal, the devices end up enrolling as personal owned. This can be changed manually on each device directly in the Intune portal after enrollment. Making sure that all devices are company owned refines management and identification, as well as enabling Intune to perform additional management tasks. Also, for additional security, you can configure device restrictions to block enrollment of devices that are not company owned.

But what if we don’t like to do stuff manually and have hundreds or thousands of devices? Automation through Microsoft Graph API and Powershell to the rescue.
Requirements
- All of this is done with inspiration from the public available Microsoft Graph API Powershell script samples on GitHub: https://github.com/microsoftgraph/powershell-intune-samples. I suggest that you head over there and dig into the scripts as a first step in this journey.
- Running anything automated (or manually for that matter) requires an account with proper permissions. A service account with delegated permissions (if not done through a Global Admin). For more information on setting up a service account with proper permissions, see this post: https://blogs.technet.microsoft.com/smeems/2017/12/18/automate-dep-assignment/#delegate
- If you really want to automate this to run unattended, you also need some sort of method for authentication. In below script, following Powershell snippet was used to create a credentials.txt file used in combination with a SA with the required permissions:
Read-Host -Prompt "Enter your tenant password" -AsSecureString | ConvertFrom-SecureString | Out-File "<Enter File Path>\credentials.txt"
- The script itself requires the Azure AD Powershell module. Run Install-Module AzureAD from an elevated Powershell prompt on the computer running the script.
How to
Following is the ManagedDevices_DeviceOwnership_Set.ps1 script from the aforementioned GitHub page with my own modifications. The original script does not have built in authentication and also requires confirmation prior to any action. This is changed in my edition as well as I included a piece that finds all personal owned devices in your tenant and changes the ownertype. The other parts of the script is not my doing.
Copy below script into Powershell ISE and update the authentication region with your own service account / credentials. To fully automate this, this can be run through a scheduled task, but I’m not covering that part here.
function Get-AuthToken {
<#
.SYNOPSIS
This function is used to authenticate with the Graph API REST interface
.DESCRIPTION
The function authenticate with the Graph API Interface with the tenant name
.EXAMPLE
Get-AuthToken
Authenticates you with the Graph API interface
.NOTES
NAME: Get-AuthToken
#>
[cmdletbinding()]
param
(
[Parameter(Mandatory=$true)]
$User,
$Password
)
$userUpn = New-Object "System.Net.Mail.MailAddress" -ArgumentList $User
$tenant = $userUpn.Host
Write-Host "Checking for AzureAD module..."
$AadModule = Get-Module -Name "AzureAD" -ListAvailable
if ($AadModule -eq $null) {
Write-Host "AzureAD PowerShell module not found, looking for AzureADPreview"
$AadModule = Get-Module -Name "AzureADPreview" -ListAvailable
}
if ($AadModule -eq $null) {
write-host
write-host "AzureAD Powershell module not installed..." -f Red
write-host "Install by running 'Install-Module AzureAD' or 'Install-Module AzureADPreview' from an elevated PowerShell prompt" -f Yellow
write-host "Script can't continue..." -f Red
write-host
exit
}
# Getting path to ActiveDirectory Assemblies
# If the module count is greater than 1 find the latest version
if($AadModule.count -gt 1){
$Latest_Version = ($AadModule | select version | Sort-Object)[-1]
$aadModule = $AadModule | ? { $_.version -eq $Latest_Version.version }
# Checking if there are multiple versions of the same module found
if($AadModule.count -gt 1){
$aadModule = $AadModule | select -Unique
}
$adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
$adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"
}
else {
$adal = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.dll"
$adalforms = Join-Path $AadModule.ModuleBase "Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll"
}
[System.Reflection.Assembly]::LoadFrom($adal) | Out-Null
[System.Reflection.Assembly]::LoadFrom($adalforms) | Out-Null
$clientId = "d1ddf0e4-d672-4dae-b554-9d5bdfd93547"
$redirectUri = "urn:ietf:wg:oauth:2.0:oob"
$resourceAppIdURI = "https://graph.microsoft.com"
$authority = "https://login.microsoftonline.com/$Tenant"
try {
$authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority
# https://msdn.microsoft.com/en-us/library/azure/microsoft.identitymodel.clients.activedirectory.promptbehavior.aspx
# Change the prompt behaviour to force credentials each time: Auto, Always, Never, RefreshSession
$platformParameters = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters" -ArgumentList "Auto"
$userId = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier" -ArgumentList ($User, "OptionalDisplayableId")
if($Password -eq $null){
$authResult = $authContext.AcquireTokenAsync($resourceAppIdURI,$clientId,$redirectUri,$platformParameters,$userId).Result
}
else {
if(test-path "$Password"){
$UserPassword = get-Content "$Password" | ConvertTo-SecureString
$userCredentials = new-object Microsoft.IdentityModel.Clients.ActiveDirectory.UserPasswordCredential -ArgumentList $userUPN,$UserPassword
$authResult = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContextIntegratedAuthExtensions]::AcquireTokenAsync($authContext, $resourceAppIdURI, $clientid, $userCredentials).Result;
}
else {
Write-Host "Path to Password file" $Password "doesn't exist, please specify a valid path..." -ForegroundColor Red
Write-Host "Script can't continue..." -ForegroundColor Red
Write-Host
break
}
}
if($authResult.AccessToken){
# Creating header for Authorization token
$authHeader = @{
'Content-Type'='application/json'
'Authorization'="Bearer " + $authResult.AccessToken
'ExpiresOn'=$authResult.ExpiresOn
}
return $authHeader
}
else {
Write-Host
Write-Host "Authorization Access Token is null, please re-run authentication..." -ForegroundColor Red
Write-Host
break
}
}
catch {
write-host $_.Exception.Message -f Red
write-host $_.Exception.ItemName -f Red
write-host
break
}
}
#region Authentication
#update info with service account and credential.txt file location
$User = "YOUR SERVICE ACCOUNT OR GLOBAL ADMIN"
$Password = "C:\Scripts\credentials.txt" #example - can be stored anywhere on the PC
write-host
# Checking if authToken exists before running authentication
if($global:authToken){
# Setting DateTime to Universal time to work in all timezones
$DateTime = (Get-Date).ToUniversalTime()
# If the authToken exists checking when it expires
$TokenExpires = ($authToken.ExpiresOn.datetime - $DateTime).Minutes
if($TokenExpires -le 0){
write-host "Authentication Token expired" $TokenExpires "minutes ago" -ForegroundColor Yellow
write-host
# Defining Azure AD tenant name, this is the name of your Azure Active Directory (do not use the verified domain name)
if($User -eq $null -or $User -eq ""){
$User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication"
Write-Host
}
$global:authToken = Get-AuthToken -User $User -Password "$Password"
}
}
# Authentication doesn't exist, calling Get-AuthToken function
else {
if($User -eq $null -or $User -eq ""){
$User = Read-Host -Prompt "Please specify your user principal name for Azure Authentication"
Write-Host
}
# Getting the authorization token
$global:authToken = Get-AuthToken -User $User -Password "$Password"
}
#endregion
Function Get-ManagedDevices(){
<#
.SYNOPSIS
This function is used to get Intune Managed Devices from the Graph API REST interface
.DESCRIPTION
The function connects to the Graph API Interface and gets any Intune Managed Device
.EXAMPLE
Get-ManagedDevices
Returns all managed devices but excludes EAS devices registered within the Intune Service
.EXAMPLE
Get-ManagedDevices -IncludeEAS
Returns all managed devices including EAS devices registered within the Intune Service
.NOTES
NAME: Get-ManagedDevices
#>
[cmdletbinding()]
param
(
[switch]$IncludeEAS,
[switch]$ExcludeMDM
)
# Defining Variables
$graphApiVersion = "Beta"
$Resource = "deviceManagement/managedDevices"
try {
$Count_Params = 0
if($IncludeEAS.IsPresent){ $Count_Params++ }
if($ExcludeMDM.IsPresent){ $Count_Params++ }
if($Count_Params -gt 1){
write-warning "Multiple parameters set, specify a single parameter -IncludeEAS, -ExcludeMDM or no parameter against the function"
Write-Host
break
}
elseif($IncludeEAS){
$uri = "https://graph.microsoft.com/$graphApiVersion/$Resource"
}
elseif($ExcludeMDM){
$uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'eas'"
}
else {
$uri = "https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managementAgent eq 'mdm' and managementAgent eq 'easmdm'"
Write-Warning "EAS Devices are excluded by default, please use -IncludeEAS if you want to include those devices"
Write-Host
}
(Invoke-RestMethod -Uri $uri -Headers $authToken -Method Get).Value
}
catch {
$ex = $_.Exception
$errorResponse = $ex.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$reader.BaseStream.Position = 0
$reader.DiscardBufferedData()
$responseBody = $reader.ReadToEnd();
Write-Host "Response content:`n$responseBody" -f Red
Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
write-host
break
}
}
# This function is slightly changed from the original and I have removed the need to confirm each action (can't automate something that needs confirmation)
Function Set-ManagedDevice(){
<#
.SYNOPSIS
This function is used to set Managed Device property from the Graph API REST interface
.DESCRIPTION
The function connects to the Graph API Interface and sets a Managed Device property
.EXAMPLE
Set-ManagedDevice -id $id -ownerType company
Returns Managed Devices configured in Intune
.NOTES
NAME: Set-ManagedDevice
#>
[cmdletbinding()]
param
(
$id,
$ownertype
)
$graphApiVersion = "Beta"
$Resource = "deviceManagement/managedDevices"
try {
if($id -eq "" -or $id -eq $null){
write-host "No Device id specified, please provide a device id..." -f Red
break
}
if($ownerType -eq "" -or $ownerType -eq $null){
write-host "No ownerType parameter specified, please provide an ownerType. Supported value personal or company..." -f Red
Write-Host
break
}
elseif($ownerType -eq "company"){
$JSON = @"
{
ownerType:"company"
}
"@
# Send Patch command to Graph to change the ownertype
$uri = "https://graph.microsoft.com/Beta/deviceManagement/managedDevices('$ID')"
Invoke-RestMethod -Uri $uri -Headers $authToken -Method Patch -Body $Json -ContentType "application/json"
Write-Host -ForegroundColor Yellow "Ownertype changed to company for device:" $ID
}
elseif($ownerType -eq "personal"){
$JSON = @"
{
ownerType:"personal"
}
"@
# Send Patch command to Graph to change the ownertype
$uri = "https://graph.microsoft.com/Beta/deviceManagement/managedDevices('$ID')"
Invoke-RestMethod -Uri $uri -Headers $authToken -Method Patch -Body $Json -ContentType "application/json"
Write-Host -ForegroundColor Yellow "Ownertype changed to personal for device:" $ID
}
}
catch {
$ex = $_.Exception
$errorResponse = $ex.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$reader.BaseStream.Position = 0
$reader.DiscardBufferedData()
$responseBody = $reader.ReadToEnd();
Write-Host "Response content:`n$responseBody" -f Red
Write-Error "Request to $Uri failed with HTTP Status $($ex.Response.StatusCode) $($ex.Response.StatusDescription)"
write-host
break
}
}
# Getting all personal owned devices
$PersonalDevices = Get-ManagedDevices | Where-Object {$_.OwnerType -eq "personal"} | Select-Object id,devicename,ownertype
# If any personal devices found
if ($PersonalDevices -ne $null) {
# Personal devices found - looping through each
ForEach ($PersonalDevice in $PersonalDevices) {
# Setting ownertype on devices
Set-ManagedDevice -id $PersonalDevice.id -ownertype Company
}
}
# No devices found - write to host
else {
Write-Host -ForegroundColor Yellow "No personal devices found"
}
Running the script
As mentioned, I’ve removed the requirement for confirmation prior to any action (to be able to run this unattended), so be careful when running the script. It will change the ownertype on ALL personal devices and write the output to the screen. I recommend that you test it on a few devices before running it full blown.

You can limit the result to a few test devices by changing following line:
Get-ManagedDevices | Where-Object {$_.OwnerType -eq "personal"} | Select-Object id,devicename,ownertype
to something like (and limit on the devicename):
Get-ManagedDevices | Where-Object {$_.OwnerType -eq "personal" -AND $_.devicename -eq "MAB"} | Select-Object id,devicename,ownertype
Also, you can of course use the same script to change the ownertype back to personal if that is required. In that case, change the last part of the script to:
# Getting all company owned devices
$CompanyDevices = Get-ManagedDevices | Where-Object {$_.OwnerType -eq "company"} | Select-Object id,devicename,ownertype
# If any company devices found
if ($CompanyDevices -ne $null) {
# Company devices found - looping through each
ForEach ($CompanyDevice in $CompanyDevices) {
# Setting ownertype on devices
Set-ManagedDevice -id $CompanyDevice.id -ownertype Personal
}
}
# No devices found - write to host
else {
Write-Host -ForegroundColor Yellow "No company devices found"
}
The entire script is running towards the Beta API, as the v1.0 API doesn’t have the ownertype data included.
Hope this was helpful 🙂
Ressources
https://github.com/microsoftgraph/powershell-intune-samples
https://blogs.technet.microsoft.com/smeems/2018/03/07/device-cleanup-with-graph-api/
Authorization Access Token is null, please re-run authentication…
$password = read-host -prompt “Enter your Password”
write-host “$password is password”
$secure = ConvertTo-SecureString $password -force -asPlainText
$bytes = ConvertFrom-SecureString $secure
$bytes | out-file .\creds.txt
i have then set the script to use creds.txt in c:\scripts
I have tried numerous encrypted txt file options which work with other scripts
Any Idea
The account generating the creds.txt file, is the one also required to run the actual script. Its unique in that way. So be aware of switching accounts in that regard. Thanks 🙂
In this script
$Password = “C:\Scripts\creds.txt”
get-content $password | convertto-securestring
Get-AuthToken -User $User -Password “$Password”
So for me to get $password which is the content of the file:
$File = “c:\scripts\creds.txt”
$Password = “password” | ConvertTo-SecureString -AsPlainText -Force
$Password | ConvertFrom-SecureString | Out-File $File
Authorization Access Token is null, please re-run authentication…
Does this script work??
The script works just fine – I tested it as recently as today. 🙂
The script has to be run as the account that created the credential file.
An app needs to be created and register over Azure AD to be used to communicate with Office graph, as the one in the script is not vaild
https://docs.microsoft.com/en-us/intune/intune-graph-apis#register-apps-to-use-the-microsoft-graph-api
Sorry, I don’t follow. There’s no app involved in the requests made towards the Graph API here. This is all powershell. See: https://github.com/microsoftgraph/powershell-intune-samples
Hi Martin,
I put in my user and password like this:
#region Authentication
#update info with service account and credential.txt file location
$User = “scurtis@blahblah.onmicrosoft.com”
$Password = “D:\Files\credentials.txt” #example – can be stored anywhere on the PC
Then I have only my password in plain text in the text file.
I’m getting:
Checking for AzureAD module…
Input string was not in a correct format.
Am I doing something wrong?
Hi Shane, you are not supposed to put in your password in clear text, but rather generate the credentials.txt file through the powershell command I provided in the post. The powershell command will prompt you for the password and then generate the .txt file as a securestring. 🙂
Hi Martin,
First of all, thanks for this great work. I am trying the script but get the following error:
Authorization Access token is null, please re-run authentication…
I am using the same account that has created the credentials.txt file to run the script. So I don’t understand what is happening.
Any feedback would be highly appreciated
I’m getting in late here. Is there an attribute for Category I can query against? So something like where $._category=”Company Owned” to select the devices and change those to Company ownership?
Works without any issues. Good work!!
we would change the owner (user) only for Microsoft Intune (MDM) registered devices (not hybrid or other type).
how can we do? i tried several ways but nothing. probably it’s yet impossible.
Francesco,
Just change add an and statement to the URI line:
$uri = “https://graph.microsoft.com/$graphApiVersion/$Resource`?`$filter=managedDeviceOwnerType eq ‘personal’ and managementAgent eq ‘mdm'”
Just wanted to drop a note. Your code is good, but if you haven’t granted approval for the Graph API in your tenant then the authentication fails, which I think is what others were seeing when they ran it.
I have taken your auth bit and modified the rest of the code so that it only gets devices from users in a specific AzureAD group. Over all VERY helpful bit of code. Thank you!
HI all, love this script. need to get it working. and thanks for the help with creating the token / password text file. but now im getting this error. what am I running wrong::
Get-ManagedDevices : Request to https://graph.microsoft.com/Beta/deviceManagement/managedDevices?$filter=managementAgent eq ‘mdm’ and managementAgent eq ‘easmdm’ failed with HTTP Status BadGateway Bad Gateway
At line:1 char:1
I already have an update. i re-ran the auth and it then worked. almost like i had to run twice. but now i have another issue. we have like 2008 devices and it only shows us to “I” not beyond. is there a limit as to how many we can convert?
Graph API which is a REST API and returns pages containing 1000 objects at the time, if you exceed 1000 you need to get the next page containing the next 1000 objects and so on until you got all the objects. This can be done by using the cmdlet Get-MSGraphAllPages.
Also, the content of this script, while it still works, it’s outdated and I would probably be using the Intune Powershell SDK (the actual Microsoft.Graph.Intune module) instead: https://github.com/Microsoft/Intune-PowerShell-SDK. I have a script example of using this here: https://www.imab.dk/script-update-automatically-remind-users-to-update-ios-with-e-mails-and-custom-notifications-using-microsoft-intune-powershell-sdk/
I was trying to modify but can’t get it work for a slightly different case.
I have a list of device names in a file and I need to change the ownertype from Personal to Company. I have 500 devices.
Could anyone help please?
Ray
Hi ,
I got the request from the team they shared only serial numbers of the devices with me and for the specific serial number’s i need to change device ownership.
Is it possible ? If yes can you please guide me with the script
Awaiting for a reply on above question
Hi ,
I got the request from the team they shared only serial numbers of the devices with me and for the specific serial number’s i need to change device ownership.
Is it possible ? If yes can you please guide me with the script
Is this script works for Unknown to Corporate as well?
Great script! Any chance this could be modified update a specific devices by devicename or serial
Great script, however it keeps coming back saying there are “No managed devices found”. We clearly have over 60 records of Personal devices within Intune. Any idea’s why it’s not picking them up? I’ve authenticated against our tenant without issues and I have the necessary modules installed. Thanks