Find Entra ID Windows devices that have signed in recently — using Microsoft Graph with certificate-based app-only auth — so you can drive a security group whose membership depends on sign-in activity (something Entra dynamic membership groups cannot do natively).
Entra ID's dynamic device membership rules do not expose any sign-in
date/time attribute — the only date/time property available is
employeeHireDate, and only for users. The supported device properties are
the well-known set (accountEnabled, deviceOSType, enrollmentProfileName,
deviceManagementAppId, deviceTrustType, isCompliant, isRooted, the
extension attributes, etc.). There is no approximateLastSignInDateTime
hook for dynamic membership.
The supported workaround is to keep an assigned security group and
synchronize its membership from an automated job. This repo is that job: a
single self-contained PowerShell script that queries Microsoft Graph and
returns the matching devices, ready to be piped into
Add-MgGroupMemberByRef / Remove-MgGroupMemberByRef.
Get-RecentlyActiveWindowsDevices.ps1 authenticates to Microsoft Graph as an
Entra app registration using an X.509 certificate (no secret, no
interactive sign-in) and returns all devices matching:
deviceOSType = Windows
accountEnabled = true
isManaged = true
trustType ≠ Workplace (i.e. AzureAd or ServerAd)
enrollmentProfileName = null
deviceManagementAppId ∈ { 0000000a-0000-0000-c000-000000000000, # Intune AAD-joined
54b943f8-d761-4f8d-951e-9cea1846db5a } # Intune Hybrid / co-managed
approximateLastSignInDateTime ≥ (UtcNow − DaysBack) # default 30 days
These criteria translate the customer-supplied Entra dynamic membership rule 1:1, plus the sign-in window that dynamic rules cannot express.
| Topic | Detail |
|---|---|
| Property name mapping | The dynamic-group rule uses device.deviceManagementAppId and device.deviceOSType. In Graph $filter the same attributes are named mdmAppId and operatingSystem. The script uses the Graph names. |
isManaged |
Not filterable on /devices. Applied client-side after the server-side filter has reduced the set. |
| Advanced query | ne, eq null and approximateLastSignInDateTime filters require advanced query params — the script sets -ConsistencyLevel eventual -CountVariable count. |
| Paging | Get-MgDevice -All is used so the SDK handles @odata.nextLink transparently. |
| Cert auth | Supports either a thumbprint (resolved from both CurrentUser\My and LocalMachine\My) or a .pfx file + SecureString password. The password is passed directly to the X509Certificate2 ctor — it is never materialized as a managed string. The private key is loaded with X509KeyStorageFlags::EphemeralKeySet. |
| Auth verification | After connecting, the script asserts Get-MgContext.AuthType -eq 'AppOnly' and bails out otherwise. |
- PowerShell 7+ (recommended) or Windows PowerShell 5.1.
- Microsoft Graph PowerShell SDK modules:
(
Install-Module Microsoft.Graph.Identity.DirectoryManagement -Scope CurrentUser
Microsoft.Graph.Authenticationis brought in as a dependency.) - An Entra app registration with:
- Application permission
Device.Read.All(admin consent granted). - A certificate uploaded to Certificates & secrets → Certificates.
- Application permission
- The corresponding certificate either:
- installed in
CurrentUser\MyorLocalMachine\My(use-CertificateThumbprint), or - available as a
.pfxfile (use-CertificatePath+-CertificatePassword).
- installed in
Tip:
TenantId,ClientIdandCertificateThumbprinthave placeholder defaults at the top of theparam()block — edit them once for your environment and you can then run the script with no arguments. The script refuses to start if any of them is still the placeholder, so it can never accidentally talk to the wrong tenant. You can still override any value on the command line.
.\Get-RecentlyActiveWindowsDevices.ps1 `
-TenantId 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' `
-ClientId '11111111-2222-3333-4444-555555555555' `
-CertificateThumbprint 'A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0' `
-DaysBack 30 `
-OutputCsv .\active-devices.csv$pwd = Read-Host -AsSecureString 'PFX password'
.\Get-RecentlyActiveWindowsDevices.ps1 `
-TenantId 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' `
-ClientId '11111111-2222-3333-4444-555555555555' `
-CertificatePath C:\certs\graph-app.pfx `
-CertificatePassword $pwd `
-DaysBack 30 `
-PassThru | Out-GridView| Parameter | Description |
|---|---|
-TenantId |
Entra tenant ID (GUID). |
-ClientId |
Application (client) ID of the Entra app registration. |
-CertificateThumbprint |
SHA‑1 thumbprint of the certificate to use (resolved from CurrentUser\My and LocalMachine\My). |
-CertificatePath |
Path to a .pfx file containing the certificate and its private key. |
-CertificatePassword |
SecureString password for the .pfx. |
-DaysBack |
Sign-in lookback window in days. Default 30, range 1..365. |
-ExcludeNameRegex |
Optional .NET regex applied (case-insensitive) to DisplayName. Any device whose name matches is excluded from the output. Example: -ExcludeNameRegex '^(KIOSK|LAB|TEST)-'. |
-OutputCsv |
Optional CSV export path (UTF-8). |
-PassThru |
Emit the device objects to the pipeline instead of pretty-printing the top 20. |
Pipe the output into the membership cmdlets:
$groupId = '<assigned-security-group-id>'
$desired = .\Get-RecentlyActiveWindowsDevices.ps1 -TenantId ... -ClientId ... `
-CertificateThumbprint ... -PassThru
$current = (Get-MgGroupMember -GroupId $groupId -All).Id
# Add
$desired.Id | Where-Object { $_ -notin $current } | ForEach-Object {
New-MgGroupMemberByRef -GroupId $groupId -BodyParameter @{
"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$_"
}
}
# Remove
$current | Where-Object { $_ -notin $desired.Id } | ForEach-Object {
Remove-MgGroupMemberByRef -GroupId $groupId -DirectoryObjectId $_
}A typical deployment runs this on a schedule (Azure Automation runbook with a Managed Identity, an AKS CronJob, a scheduled task, etc.).
approximateLastSignInDateTimehas a latency of a few hours and reflects the device sign-in, not the user's sign-in on that device. If you need user-level sign-ins, queryauditLogs/signInsinstead and aggregate bydeviceDetail.deviceId.- The
Device.Read.Allpermission is application-level and returns the entire tenant device inventory; treat the output as confidential.