Skip to main content

Regularly clean up MEID devices with automation

Requirements: Create an App Registration with Directory.ReadWrite.All and Device.ReadWrite.All. For logging you need to have an Azure Function to create an API to centralize logs in an Azure Log Analytics Workspace. 

This automation gets all Microsoft Entra ID Devices via the Microsoft Graph API and checks everyone for their latest sign in. If the login was more than 6 months ago, the device will be deleted from the Microsoft Entra ID. Afterwards, the key figures for the deleted devices and the errors are passed to the Azure Function API, where they are then stored in the corresponding Log Analytics Workspace.

Use case

This automation concept is perfectly suited for large enterprises which don't have an overview of their old Microsoft Entra ID Devices. Authentication in the direction of Microsoft Entra ID is user-independent with an app registration. This makes it perfect for automated and regular execution of this code. In combination with an Azure Runbook that runs weekly, the Microsoft Entra ID environment can be simplified significantly.

PowerShell script

This PowerShell script needs the AzureAD Module, an app registration with the appropriate permissions and an Azure Function to collect the logs and write them to an Azure Log Analytics Workspace.

First install the AzureAD Module.

Install-Module AzureAD

After you have installed the modules and organized the prerequisites (App Registration with Directory.ReadWrite.All and Device.ReadWrite.All permission and an Azure Function API for centralizing the logs) you can fill the four variables to the PowerShell code and run the script.

Import-Module AzureAD

$tenantId= "<yourtenantid>"
$ClientId= "<yourclientidofappregistration>"
$ClientSecret = "<yourclientsecretofappregistration>"

$expirationThresholdMonths = 6

Import-Module AzureAD

Function Send-Logs(){

    param (
    $LogBodyJSON = @"
        "logtype": "$LogType",
        "logbody": {}

    $LogBodyObject = ConvertFrom-JSON $LogBodyJSON
    Foreach($Log in $LogBodyList.GetEnumerator()){
        $LogBodyObject.logbody | Add-Member -MemberType NoteProperty -Name $log.key -Value $log.value
    $Body = ConvertTo-JSON $LogBodyObject
    $Response = Invoke-Restmethod -uri $url -Body $Body -Method POST -ContentType "application/json"
    return $Response

$Body = @{
"tenant" = $TenantId
"client_id" = $ClientId
"scope" = ""
"client_secret" = $ClientSecret
"grant_type" = "client_credentials"
$Params = @{
"Uri" = "$TenantId/oauth2/v2.0/token"
"Method" = "Post"
"Body" = $Body
"ContentType" = "application/x-www-form-urlencoded"
$AuthResponse = Invoke-RestMethod @Params

$Header = @{
    "Authorization" = "Bearer $($AuthResponse.access_token)"

$azurePassword = ConvertTo-SecureString $clientSecret -AsPlainText -Force
$psCred = New-Object System.Management.Automation.PSCredential($ClientId , $azurePassword)
Connect-AzAccount -Credential $psCred -TenantId $TenantId -ServicePrincipal

$context = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile.DefaultContext
$aadToken = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate($context.Account, $context.Environment, $context.Tenant.Id.ToString(), $null, [Microsoft.Azure.Commands.Common.Authentication.ShowDialog]::Never, $null, "").AccessToken
Connect-AzureAD -AadAccessToken $aadToken -AccountId $context.Account.Id -TenantId $

$Result = Invoke-RestMethod -Method GET -Uri "" -Header $Header
$Devices = $Result.value
while ($Result.'@odata.nextLink') {
    $Result = Invoke-RestMethod -Uri $Result.'@odata.nextLink' -Headers $Header
    $Devices += $Result.value

$oldDevices = @()
$failedDateDevices = @()
foreach($inactiveDevice in $Devices){
        $Date = Get-Date($inactiveDevice.approximateLastSignInDateTime)
        if($Date -lt ((Get-Date).AddMonths(-$expirationThresholdMonths))){
            $oldDevices += $inactiveDevice
    } catch {
        $failedDateDevices += $inactiveDevice

Write-Warning "Date Errors: $($failedDateDevices.count)"

$failedDeleteDevices = @()
$deletedDevices = @()
foreach($Device in $oldDevices){
        Remove-AzureADDevice -ObjectId $
        Write-Output "Deleted: $($Device.displayName)"
        $deletedDevices += $Device
        $failedDeleteDevices += $Device
Write-Warning "Delete Errors: $($failedDeleteDevices.count)"

$Logs = @{
    "deletesuccess" = "$($deletedDevices.count)"

Send-Logs -LogType "CleanUpMEIDDevicesExecutions" -LogBodyList $Logs