Change device ownership in Microsoft Intune standalone using Microsoft Graph API and Powershell

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/

25 thoughts on “Change device ownership in Microsoft Intune standalone using Microsoft Graph API and Powershell”

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

    Reply
    • 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 🙂

      Reply
  2. 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??

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

      Reply
  3. 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?

    Reply
    • 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. 🙂

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

    Reply
  5. 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?

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

    Reply
    • 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'”

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

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

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

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

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

    Reply

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.