prod@blog:~$

Detecting New Microsoft Authenticator Registrations: Filtering Signal from Noise in Azure AD Audit Logs

When I needed to track which users were registering new Microsoft Authenticator devices in our organization, I initially thought it would be straightforward. Query the audit logs for authentication-related events, filter for the ones I wanted, and export the results. What I discovered was that the audit logs contain a significant amount of noise around authentication events, making it challenging to identify when users actually register NEW devices versus when they simply use existing ones or when tokens refresh.

This blog post covers how I built a PowerShell solution to detect genuine new Microsoft Authenticator registrations by parsing Azure AD audit logs, focusing specifically on filtering out false positives to identify only the events that matter: when users activate new authenticator clients.

Visual Output

This is the script running for the last hour:


The Problem: Too Much Noise in the Signal

Initially, I wrote a simple script that looked for any StrongAuthenticationPhoneAppDetail modifications in the audit logs. The results were overwhelming—users appeared to be "registering" authenticator apps dozens of times per day. Here's what I was seeing for a single user:

2026-01-15T08:22:11Z,john.doe@company.com,Update user,iPhone SE (2nd generation)
2026-01-15T09:15:33Z,john.doe@company.com,Update user,iPhone SE (2nd generation)
2026-01-15T10:42:18Z,john.doe@company.com,Update user,iPhone SE (2nd generation)
2026-01-15T11:33:22Z,john.doe@company.com,Update user,iPhone SE (2nd generation)

These weren't new registrations—they were token refreshes, re-authentications, and other maintenance events. The user had registered their iPhone once, but my script was flagging every interaction as a "new registration."

Understanding the Audit Log Structure

To solve this, I needed to understand what the audit logs actually contain. Each StrongAuthenticationPhoneAppDetail event includes both an oldValue and newValue field. The key insight was that genuine new registrations show a difference between these values—either the old value is empty, or the new value contains additional devices not present in the old value.

Here's how I query the audit logs to get the raw events:

# Calculate the date filter
if ($HoursBack -gt 0) {
    $startDate = (Get-Date).AddHours(-$HoursBack).ToString("yyyy-MM-ddTHH:mm:ssZ")
} else {
    $startDate = (Get-Date).AddDays(-$DaysBack).ToString("yyyy-MM-ddTHH:mm:ssZ")
}

# Build the filter query
$filter = "activityDateTime ge $startDate and " +
          "category eq 'UserManagement' and " +
          "activityDisplayName eq 'Update user' and " +
          "result eq 'success'"

$url = "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=$filter&`$top=999"

The False Positive Problem

I realized that each audit event contains ALL registered devices for a user, not just the one being added or modified. A user with multiple devices (iPhone, Android tablet, work phone) would show all devices in every update event, even if only one was being used or refreshed.

This meant I was getting entries like this for a single audit event:

2026-01-15T12:22:11Z,user@company.com,iPhone SE (2nd generation),Device1Token
2026-01-15T12:22:11Z,user@company.com,CPH2493 (Android),Device2Token  
2026-01-15T12:22:11Z,user@company.com,SM-S936B,Device3Token

All three entries had the same timestamp because they came from one audit event showing the user's complete device list. Only one of these might actually be new, but my initial script treated all three as separate new registrations.

The Solution: Comparing Old and New Values

You need to parse both the oldValue and newValue fields from each audit event to identify what actually changed:

# Parse old and new values to detect additions
$oldDevices = @()
$newDevices = @()

if ($modifiedProperty.oldValue -and $modifiedProperty.oldValue -ne "[]") {
    try {
        $oldDevices = $modifiedProperty.oldValue | ConvertFrom-Json
    } catch {}
}

if ($modifiedProperty.newValue -and $modifiedProperty.newValue -ne "[]") {
    try {
        $newDevices = $modifiedProperty.newValue | ConvertFrom-Json
    } catch {}
}

# Identify newly added devices
foreach ($newDevice in $newDevices) {
    $isNew = $true
    
    # Check if this device existed in old value
    foreach ($oldDevice in $oldDevices) {
        if ($oldDevice.DeviceToken -eq $newDevice.DeviceToken) {
            $isNew = $false
            break
        }
    }
    
    # This is a genuinely new registration
    if ($isNew) {
        # Skip "NO_DEVICE" entries (these are SMS/Phone call methods)
        if ($newDevice.DeviceName -ne "NO_DEVICE") {
            # Process as new registration
        }
    }
}

This approach uses the unique DeviceToken as the identifier to determine if a device existed before this audit event. Only devices that appear in the new value but not in the old value are considered genuine new registrations.

Setting Up the App Registration

To query the audit logs, I needed an App Registration with the correct permissions:

Required Application Permissions:

  • AuditLog.Read.All - Read audit log events
  • Directory.Read.All - Read user directory information
function Get-GraphAccessToken {
    param(
        [string]$TenantId,
        [string]$AppId,
        [string]$ClientSecret
    )
    
    $tokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
    
    # Build the request body as a URL-encoded string
    $body = "client_id=$AppId&scope=https://graph.microsoft.com/.default&client_secret=$ClientSecret&grant_type=client_credentials"
    
    try {
        $response = Invoke-RestMethod -Method Post -Uri $tokenEndpoint -Body $body -ContentType "application/x-www-form-urlencoded"
        return $response.access_token
    }
    catch {
        Write-Error "Failed to obtain access token: $_"
        exit 1
    }
}

Flexible Time Periods

I extended the script to support various time periods, which proved invaluable for different use cases:

param(
    [Parameter(Mandatory=$false)]
    [ValidateSet("1hour", "24hours", "2days", "5days", "7days", "10days", "15days", "30days")]
    [string]$TimePeriod = "30days"
)

switch ($TimePeriod) {
    "1hour"    { $HoursBack = 1; $PeriodDescription = "1 hour" }
    "24hours"  { $HoursBack = 24; $PeriodDescription = "24 hours" }
    "7days"    { $DaysBack = 7; $PeriodDescription = "7 days" }
    "30days"   { $DaysBack = 30; $PeriodDescription = "30 days" }
}

Sample Output and Usage

When I run the script now, I get clean, actionable results:

.\Get-AuthenticatorRegistrations.ps1 -TimePeriod 7days
NEW Microsoft Authenticator Registration Audit Report
=====================================================
Time Period: 7 days
Searching for NEW registrations in the last 7 days

Authenticating to Microsoft Graph...
Querying audit logs for the last 7 days...
Found 342 total StrongAuthenticationPhoneAppDetail events
Analyzing events to identify NEW registrations...

Results exported to: .\NEW_MFA_Authenticator_Registrations_7days_20260115_143022.csv

Summary of NEW Registrations:
------------------------------
Total NEW registrations: 18
Unique users with NEW registrations: 18

Device types newly registered:
  iPhone 15 Pro: 6
  iPhone SE (2nd generation): 4
  SM-S921B: 4
  Pixel 7: 3
  CPH2493: 1

Daily breakdown of NEW registrations:
  2026-01-09: 2 new registrations
  2026-01-10: 4 new registrations
  2026-01-11: 1 new registrations
  2026-01-12: 6 new registrations
  2026-01-13: 3 new registrations
  2026-01-14: 2 new registrations

Most recent 10 NEW registrations:
DateTime                      UserPrincipalName              DeviceName
--------                      -----------------              ----------
2026-01-14T15:42:13.000Z     sarah.jones@croucher.cloud    iPhone 15 Pro
2026-01-14T09:23:41.000Z     mike.wilson@croucher.cloud    SM-S921B
2026-01-13T16:55:02.000Z     emma.brown@croucher.cloud     Pixel 7

Notice how the script found 342 total events but identified only 18 as genuine new registrations—a dramatic reduction in false positives.

The CSV Output Structure

The script generates a detailed CSV with the correct device identifiers:

$registration = [PSCustomObject]@{
    DateTime                = $event.activityDateTime
    UserPrincipalName      = $targetUser.userPrincipalName
    UserId                 = $targetUser.id
    Action                 = "New Authenticator Registration"
    DeviceName            = $newDevice.DeviceName
    DeviceToken           = $newDevice.DeviceToken
    PhoneAppVersion       = $newDevice.PhoneAppVersion
    AuthenticationType    = switch ($newDevice.AuthenticationType) {
        3 { "Push Notification" }
        2 { "OTP" }
        1 { "Phone Call" }
        default { "Type_$($newDevice.AuthenticationType)" }
    }
    AuthenticatorFlavor   = $newDevice.AuthenticatorFlavor
    RegistrationMethod    = if ($newDevice.DeviceTag -eq "SoftwareTokenActivated") { "App Registration" } else { $newDevice.DeviceTag }
    CorrelationId         = $event.correlationId
}

Each row in the CSV represents a genuine new device registration with its unique device token and registration timestamp, enabling precise tracking of when users activated new authenticator clients.

Conclusion

By focusing on the difference between old and new values in audit events rather than just the presence of authentication-related events, I was able to eliminate false positives and create a reliable tool for tracking genuine new Microsoft Authenticator registrations. The key insight was understanding that each audit event represents the complete state of a user's authentication methods, not just the changed portion.

This approach provides clean, actionable data about when users actually register new devices, making it invaluable for tracking MFA adoption progress and identifying users who may need additional support with their authentication setup.

The script, with its flexible time periods and detailed output, has become an essential tool for our security operations, providing both immediate operational insights and longer-term trend analysis for our MFA migration initiatives.