This repository deploys an Azure-based data collection pipeline to identify users who still sign in with SMS or voice authentication and to compare that usage with the authentication methods they have already registered in Microsoft Entra ID.
The solution exists to answer a practical retirement question:
- Which users are still actively using SMS or voice as a sign-in method?
- Which of those users already have stronger methods registered, such as passkeys or other passwordless methods?
- Which applications are still associated with recent SMS or voice usage?
The deployment stores Microsoft Graph userRegistrationDetails data in a custom Log Analytics table and then correlates that data with sign-in telemetry by using KQL.
Retiring telephony-based authentication requires more than a policy change. Before disabling SMS or voice, you need evidence that shows:
- who is still using those methods,
- whether safer alternatives are already registered,
- and where the remaining operational impact is likely to be.
Microsoft Graph provides the registration posture, but it does not provide your investigation-ready history inside Log Analytics by default. This repository bridges that gap by:
- querying Microsoft Graph for user registration details,
- normalizing that data into a custom Log Analytics table,
- and making it available for hunting and reporting alongside sign-in logs.
The solution uses four components:
- A Log Analytics custom table to store user registration posture.
- A Data Collection Rule (DCR) that defines the ingestion schema and destination table.
- A Logic App with a system-assigned managed identity that calls Microsoft Graph and posts records to the Logs Ingestion API.
- A PowerShell script that grants the managed identity the Microsoft Graph application permission it needs.
Flow:
- The Logic App runs on a schedule.
- It calls
https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?$top=1000by using its managed identity. - It loops through the returned records.
- It posts each record to the DCR Logs Ingestion endpoint.
- Azure Monitor writes the data into
MfaUserRegistration_CL. - You query that table together with sign-in logs to identify SMS and voice dependencies.
mfa-table.json: creates the custom Log Analytics tableMfaUserRegistration_CL.dcr-mfa.json: creates the DCR, the custom stream declaration, and the data flow to the custom table.logicApp-mfa.json: creates the Logic App, enables a system-assigned managed identity, and assignsMonitoring Metrics Publisheron the DCR.permissionsGraph-SP.ps1: assigns the Microsoft Graph application permissionAuditLog.Read.Allto the Logic App managed identity by resolving the Logic App service principal from its display name.
Before deployment, make sure you have:
- an Azure subscription,
- an existing Log Analytics workspace,
- rights to deploy ARM templates in the target resource group,
- rights to assign Azure RBAC roles on the DCR,
- rights to assign Microsoft Graph application permissions to a service principal,
- Azure CLI installed and authenticated,
- Microsoft Graph PowerShell SDK installed for the permission assignment script.
The Log Analytics workspace must already exist. This repository does not create the workspace itself.
Two permission models are used in this solution.
The Logic App managed identity needs permission to send data to the DCR. The template handles this automatically by assigning:
Monitoring Metrics Publisheron the DCR scope.
The Logic App managed identity calls Microsoft Graph by using managed identity authentication. To read userRegistrationDetails, the managed identity must receive the Graph application permission used by this repository:
AuditLog.Read.All
That permission is not granted by the ARM templates. It is assigned separately by the PowerShell script in this repository.
The order matters because each step depends on the previous one.
Deploy mfa-table.json first.
az deployment group create `
--resource-group <resource-group> `
--template-file mfa-table.json `
--parameters workspaceName=<log-analytics-workspace-name>This creates the MfaUserRegistration_CL table in your Log Analytics workspace.
Deploy dcr-mfa.json second.
First, retrieve the exact workspace resource ID:
$workspaceResourceId = az monitor log-analytics workspace show `
--resource-group <workspace-resource-group> `
--workspace-name <log-analytics-workspace-name> `
--query id -o tsvThen deploy the DCR:
az deployment group create `
--resource-group <resource-group> `
--template-file .\dcr-mfa.json `
--parameters workspaceResourceId="$workspaceResourceId" workspaceLocation=<workspace-location>Important:
workspaceResourceIdmust be the full Azure resource ID, not the workspace GUID.workspaceLocationmust match the Log Analytics workspace region.
Expected format for workspaceResourceId:
/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.OperationalInsights/workspaces/<workspace-name>
Deploy logicApp-mfa.json after the DCR exists.
az deployment group create `
--resource-group <resource-group> `
--template-file .\logicApp-mfa.json `
--parameters logicAppName=<logic-app-name> dcrName=dcr-mfa-user-registrationThis deployment:
- creates the Logic App,
- enables a system-assigned managed identity,
- reads the DCR ingestion endpoint and immutable ID,
- and assigns
Monitoring Metrics Publisherto the managed identity on the DCR.
After the Logic App is created, update the variables in permissionsGraph-SP.ps1:
$TenantIDwith your Entra tenant ID,$DisplayNameMIwith the Logic App name,- and keep
$GraphPermissionset toAuditLog.Read.Allunless you intentionally change the API permission model.
The script then:
- connects to Microsoft Graph with the required delegated scopes,
- finds the Logic App service principal by display name,
- resolves the Microsoft Graph service principal,
- and assigns the application role directly to the managed identity.
Run it with:
pwsh .\permissionsGraph-SP.ps1The script assigns AuditLog.Read.All to the Logic App managed identity against Microsoft Graph.
mfa-table.json defines the target schema. The columns are aligned with the user registration details returned by Microsoft Graph, plus TimeGenerated for Log Analytics ingestion.
dcr-mfa.json defines:
- the custom stream name,
- the schema expected by the ingestion endpoint,
- the Log Analytics destination,
- and the mapping from the incoming custom stream to
MfaUserRegistration_CL.
logicApp-mfa.json implements the scheduled workflow:
- trigger on a daily recurrence by default,
- call Microsoft Graph using managed identity authentication,
- initialize an array from the returned
valuepayload, - loop over the records,
- add
TimeGenerated, - and post each item to the Logs Ingestion API.
permissionsGraph-SP.ps1 uses Microsoft Graph PowerShell to assign the Microsoft Graph app role directly to the Logic App managed identity service principal. It does not require you to manually copy the managed identity object ID into the script.
Use these checks to confirm the deployment is working.
az monitor data-collection rule show `
--resource-group <resource-group> `
--name dcr-mfa-user-registrationaz resource show \
--resource-group <resource-group> `
--name <logic-app-name> `
--resource-type Microsoft.Logic/workflows `
--query identityRun a query such as:
MfaUserRegistration_CL
| take 10If no data appears, review the Logic App run history and confirm both permissions are in place:
Monitoring Metrics Publisheron the DCR,AuditLog.Read.Allon the managed identity service principal.
Also confirm that $DisplayNameMI in the PowerShell script exactly matches the Logic App resource name.
After the pipeline is ingesting successfully, use the following query to identify users who recently used SMS or voice and already have stronger methods registered.
let Registration =
MfaUserRegistration_CL
| extend UPN = tolower(userPrincipalName)
| extend Methods = todynamic(methodsRegistered)
| summarize arg_max(TimeGenerated, *) by UPN
| extend
HasMobilePhone = Methods has "mobilePhone",
HasOfficePhone = Methods has "officePhone",
HasPasskey = Methods has "passKeyDeviceBound" or Methods has "passkeySynced";
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType == 0
| mv-expand AuthDetail = todynamic(AuthenticationDetails)
| extend
UPN = tolower(UserPrincipalName),
UsedMethod = tostring(AuthDetail.authenticationMethod),
StepSucceeded = tobool(AuthDetail.succeeded)
| where StepSucceeded
| where UsedMethod in ("Text message", "Voice call")
| summarize
UsageCount = count(),
LastUsage = max(TimeGenerated),
Applications = make_set(AppDisplayName, 20)
by UPN, UsedMethod
| join kind=leftouter Registration on UPN
| where HasPasskey == true
| project
UPN,
methodsRegistered = Methods,
defaultMfaMethod,
HasMobilePhone,
HasOfficePhone,
HasPasskey,
UsedMethod,
UsageCount,
LastUsage,
Applications
| order by LastUsage descThis is the most common deployment mistake for dcr-mfa.json.
Use the full Azure resource ID, not:
- the workspace customer ID,
- the workspace GUID,
- or a partial resource path.
Retrieve it with Azure CLI instead of typing it manually.
If the DCR is created but the deployment output evaluation fails, verify the API version support in your CLI or deployment tooling and confirm the DCR can be queried directly afterward.
Confirm that:
- the managed identity exists,
- the Graph app role assignment completed successfully,
- and admin consent requirements in your tenant are satisfied.
Confirm that:
- the DCR exists,
- the Logic App has
Monitoring Metrics Publisheron the DCR, - the DCR points to the correct workspace,
- and the stream schema matches the custom table schema.
- Azure Monitor Logs Ingestion API overview: https://learn.microsoft.com/azure/azure-monitor/logs/logs-ingestion-api-overview
- Azure Monitor tutorial with ARM templates: https://learn.microsoft.com/azure/azure-monitor/logs/tutorial-logs-ingestion-api
- Azure Monitor prerequisites for Logs Ingestion API: https://learn.microsoft.com/azure/azure-monitor/logs/set-up-logs-ingestion-api-prerequisites