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