In an increasingly automated world, the quality and accuracy of data are more critical than ever. Today’s IT environments especially cloud-based directories like Microsoft Entra ID rely heavily on correct user attributes to drive automation. Concepts, designs, and access frameworks often depend entirely on these attributes being reliable. User Attribute Issues are often generate trough Systems or User mistakes.
Many companies are already leveraging automation to streamline operations. For instance, when a user’s country attribute equals "Switzerland", they may be automatically added to Microsoft Teams and dynamic security groups like Team-Switzerland-Users. This can greatly simplify onboarding and policy enforcement but it also introduces risk.
The challenge? What happens when those user attributes are incorrect, incomplete, or outdated?
In real-world environments, we’ve observed issues like:
1. Users missing from critical security or distribution groups.
2. Incorrect access provisioning through Conditional Access or Access Packages.
3. Compliance workflows breaking due to inconsistent Department, Country, or Company fields.
These misconfigurations can quietly undermine the entire automation layer leading to access issues, security gaps, or audit failures. So the question becomes:
How can we monitor user attributes in Entra ID to prevent these problems before they impact our environment?
To address this, I’ve developed a solution that monitors and validates user attributes across the tenant. In this blog post, I’ll walk you through how it works from data sourcing to automation and how it helps ensure your Entra ID environment remains accurate, secure, and automation-ready.
Table of Contents
Solution Architecture / Components
Technical Components:

Email Alerting [Outcome]:

Requirements
- Azure Subscription
- Automation Account
- Powershell Runbook with the right modules installed: AzTable, Microsoft.Graph.Authentication, Microsoft.Graph.Users
- Logic Apps (Standard or Consumption)
- Storage Account Table for storing the VaildValues for User Attributes
- Microsoft Graph API connection with
User.Read.AllorDirectory.Read.All - Office 365 Outlook connector with mailbox access
Setup Managed Identities
Securing cloud automation involves more than just writing good code, it means controlling access tightly and using the principle of least privilege throughout your architecture. In Azure, Managed Identities play a central role in doing this securely and without storing credentials in code.
My Goal was to create a Solution based on Managed Identities, let me walkthrough how I archive this.
- Automation Account
- Azure Table Storage
- Microsoft Graph API
- Entra ID (Directory Reader role)
- Logic App
- Calling the Automation Account Runbook
What is a Managed Identity?
A Managed Identity in Azure is an automatically managed identity in Entra ID that can be used to authenticate to any service that supports Entra ID authentication. There are two types:
- System-assigned: Tied to the lifecycle of the resource.
- User-assigned: Standalone and sharable across resources.
With a managed identity, you don’t need to store credentials in your script, app settings, or Logic App.
Automation Account: Secure Access with Managed Identity
Enable System-Assigned Identity
- Go to your Automation Account in Azure.
- Under the Identity blade, enable System-assigned Managed Identity.
- After enabling, Azure will create a service principal in Entra ID for this resource.
Grant Access to Table Storage
To read/write Azure Table data:
- Go to your Storage Account.
- Under Access Control (IAM), click + Add role assignment.
- Assign the role Contributor to the Automation Account’s managed identity. If you face problem check maybe the Subscription or Resource Group Permissions (e.g. Reader)
Grant Directory Reader Role for Entra ID
To access user and group information via Microsoft Graph:
- Go to Microsoft Entra ID → Roles and administrators.
- Find and select Directory Readers.
- Click + Add assignments, and add your Automation Account’s managed identity.
Grant Permissions to Microsoft Graph API
Azure doesn’t directly allow assigning Microsoft Graph application permissions to a managed identity via portal. Follow this detailed guide to grant permissions using Microsoft Graph PowerShell:
📎 Reference: How to connect MGGraph Identity with Managed Identities
Script summarized from the article:
# Connect to Microsoft Graph with least required permission scope
Connect-MgGraph -Scopes Application.Read.All, AppRoleAssignment.ReadWrite.All
### Define these variables here first ##
$ManagedIdentityName = "cdf-automation-account"
$permissions = "User.Read.All", "Directory.Read.All"
# Get service principal and roles
$getPerms = (Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'").approles | Where {$_.Value -in $permissions}
$ManagedIdentity = (Get-MgServicePrincipal -Filter "DisplayName eq '$ManagedIdentityName'")
$GraphID = (Get-MgServicePrincipal -Filter "AppId eq '00000003-0000-0000-c000-000000000000'").id
# Assign roles
foreach ($perm in $getPerms){
New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $ManagedIdentity.Id `
-PrincipalId $ManagedIdentity.Id -ResourceId $GraphID -AppRoleId $perm.id
}
Logic App: Using Managed Identity to Call the Automation Account
Enable Managed Identity for Logic App
- Go to your Logic App.
- Under Identity, enable System-assigned Managed Identity.
Grant Access to Automation Account
To allow your Logic App to trigger a Runbook in the Automation Account:
- Go to the Automation Account.
- Under Access Control (IAM), add the Contributor role to your Logic App’s managed identity.
- This lets the Logic App start a Runbook
With Managed Identities, your Automation Account and Logic App can securely access Azure Storage, Microsoft Graph, and each other without ever handling secrets. There is definitely room for improvement here because to really go with the least privilege approach you need to reduce the Contributor Roles to more fitting ones.
Setup Storage Account Table
Creating an Azure Storage Account
To create a Storage Account in Azure, follow these steps:
- Log in to the Azure Portal: Open your web browser and navigate to Azure Portal. Log in using your Azure credentials.
- Create a New Storage Account:
- In the left-hand menu, select Create a resource.
- In the Search the Marketplace field, type in Storage Account, then select Storage account.
- Click Create to start the configuration process.
- Configure Your Storage Account:
- Subscription: Choose the subscription where you want the Storage Account to be created.
- Resource Group: You can either create a new resource group or select an existing one.
- Storage Account Name: Provide a unique name for your storage account. This name will be used in the URL
(e.g., sthcmprovisioning2024).
- Review and Create:
- After filling in all the required fields, click Review + Create.
- Review the settings and click Create to provision the Storage Account.
Creating a Table in Your Storage Account
Once your Storage Account is created, you can add a Table to store your data. Follow these steps to create a Table:
- Access the Storage Account:
- Navigate to your newly created Storage Account in the Azure Portal.
- Create a Table:
- In the left-hand menu of the Storage Account, scroll down to the Table option under the Data storage section.
- Click + Table to start creating your Table.
- Enter a name for your table
(e.g., UserAttributesValidation), and click OK to create it.
Adding Valid Values into the Table
Now that your table is created, let’s populate it with valid values. Azure Storage Tables use a schema of PartitionKey, RowKey, and other columns for data storage.
Here’s how to input data into your table:
- Navigate to the Table:
- Inside the Storage Account, click on Storage Browser -> Tables, and then select the table you created
(e.g., UserAttributesValidation).
- Inside the Storage Account, click on Storage Browser -> Tables, and then select the table you created

- Add Entities:
- To add data, click on + Add Entity. You’ll need to define each record using the following key fields:
- PartitionKey: This is used for the User Attributes (e.g.
CompanyName, Department, County and ExcludedUsers) - RowKey: A unique identifier for each record within a partition. This could be a unique string like
(e.g. ValidValues) - ValidValues: This column as Type String is used for storing the default valid Values for your organization.
(e.g. ["cloudfil.ch", "contoso.com"])
- PartitionKey: This is used for the User Attributes (e.g.
- To add data, click on + Add Entity. You’ll need to define each record using the following key fields:
| PartitionKey | RowKey | ValidValues |
|---|---|---|
| CompanyName | ValidValues | [“cloudfil.ch”, “contoso.com”] |
| Country | ValidValues | [“Switzerland”, “Germany”] |
| Department | ValidValues | [“IT”,”Digital Transformation”,”HR”,”Marketing”,”Sales”] |
| ExcludedUsers | ValidValues | [“bg1@xyz.ch”,”bg2@xyz.ch”] |
Setup Automation Account and Runbook
Azure Automation Accounts allow you to automate frequent, time-consuming, and error-prone cloud management tasks. In this chapter, we’ll cover how to set up an Automation Account and create a Runbook using PowerShell.
Create an Automation Account
Follow these steps to set up a new Automation Account in the Azure Portal:
- Navigate to Automation Accounts
Use the search bar at the top to find Automation Accounts. - Click “+ Create”
- Subscription: Select your active subscription.
- Resource Group: Use an existing one or create a new group.
- Automation Account Name: Choose a unique name
(e.g., cdf-automation-account). - Region: Select the Azure region where the automation will run.
- Click “Review + Create”, then Create once validation passes.
Add necessary Modules
For our Solution we need specific PowerShell Modules, which first have to be loaded to the Automation Accounts.
- Navigate to Your Automation Account
From the Automation Account blade, go to Modules in the left-hand menu. - Click “Add a module”
Now Browse from gallery the following Modules:- AzTable
- Microsoft.Graph.Authentication
- Microsoft.Graph.Users
- Select Runtime version
The Solution is only tested under Runtime version 5.1 - Finish with “Import”

Create a Runbook
A Runbook is a container for automation logic, often in PowerShell. Here’s how to create a Runbook.
- Navigate to Your Automation Account
From the Automation Account blade, go to Runbooks in the left-hand menu. - Click “+ Create a Runbook”
- Name:
User-MisMatch-Alerting-v5 - Runbook Type: PowerShell
- Runbook version: 5.1
- Click Create.
- Name:
- Insert Code
Once in the editor, paste the following code:
# Required modules
$requiredModules = @("AzTable", "Microsoft.Graph.Authentication", "Microsoft.Graph.Users")
# Function to wait for modules to be loaded
function Wait-ForModules {
param (
[string[]]$Modules,
[int]$TimeoutSeconds = 60
)
$elapsed = 0
$interval = 5
while ($elapsed -lt $TimeoutSeconds) {
$loadedModules = Get-Module -ListAvailable | Where-Object { $Modules -contains $_.Name } | Select-Object -ExpandProperty Name -Unique
$missingModules = $Modules | Where-Object { $_ -notin $loadedModules }
if ($missingModules.Count -eq 0) {
Write-Host "All required modules loaded: $($Modules -join ', ')"
return
} else {
Write-Host "Waiting for modules to load: $($missingModules -join ', ')"
Start-Sleep -Seconds $interval
$elapsed += $interval
}
}
throw "Modules not loaded in time: $($missingModules -join ', ')"
}
# Wait for all modules to be loaded
Wait-ForModules -Modules $requiredModules
# Import modules
Import-Module AzTable
Import-Module Microsoft.Graph.Users
Import-Module Microsoft.Graph.Authentication
# Log in with Managed Identity
$null = Connect-AzAccount -Identity
# Set variables
$resourceGroup = "rg-HcmProvisioning"
$storageAccountName = "sthcmprovisioning2024"
$tableName = "UserAttributesValidation"
# Get storage account and table context
$storageAccount = Get-AzStorageAccount -ResourceGroupName $resourceGroup -Name $storageAccountName
$ctx = $storageAccount.Context
$storageTable = Get-AzStorageTable -Name $tableName -Context $ctx
$cloudTable = $storageTable.CloudTable
$rows = Get-AzTableRow -Table $cloudTable
# Parse the retrieved rows and check for valid 'ValidValues'
$parsed = $rows | ForEach-Object {
if ($_.ValidValues -ne $null) {
[PSCustomObject]@{
PartitionKey = $_.PartitionKey
RowKey = $_.RowKey
ValidValues = (ConvertFrom-Json $_.ValidValues)
}
} else {
Write-Host "ValidValues is null for RowKey: $($_.RowKey)"
}
}
$companyName = ($parsed | Where-Object { $_.PartitionKey -eq "CompanyName" }).ValidValues
$country = ($parsed | Where-Object { $_.PartitionKey -eq "Country" }).ValidValues
$department = ($parsed | Where-Object { $_.PartitionKey -eq "Department" }).ValidValues
$excludedUsers = ($parsed | Where-Object { $_.PartitionKey -eq "ExcludedUsers" }).ValidValues
# Log in with Managed Identity
Connect-MgGraph -Identity -NoWelcome
# Fetch users
$users = Get-MgUser -All -Property Id, AccountEnabled, createdDateTime, DisplayName, UserPrincipalName, companyName, country, department, UserType |
Select-Object Id, AccountEnabled, createdDateTime, DisplayName, UserPrincipalName, companyName, country, department, UserType
# Exclude users where UserPrincipalName is in ExcludedUsers list or UserType is "Guest"
$filteredUsers = $users | Where-Object {
$excludedUsers -notcontains $_.UserPrincipalName -and $_.UserType -ne "Guest" -and $_.AccountEnabled -eq "True"
}
$mismatchReport = @()
foreach ($user in $filteredUsers) {
$invalidCompanyName = if (-not $user.companyName) {
"EMPTY"
} elseif ($companyName -notcontains $user.companyName) {
$user.companyName
} else {
""
}
$invalidCountry = if (-not $user.country) {
"EMPTY"
} elseif ($country -notcontains $user.country) {
$user.country
} else {
""
}
$invalidDepartment = if (-not $user.department) {
"EMPTY"
} elseif ($department -notcontains $user.department) {
$user.department
} else {
""
}
if ($invalidCompanyName -or $invalidCountry -or $invalidDepartment) {
$mismatchedAttributes = @()
if ($invalidCompanyName) { $mismatchedAttributes += "CompanyName" }
if ($invalidCountry) { $mismatchedAttributes += "Country" }
if ($invalidDepartment) { $mismatchedAttributes += "Department" }
$mismatchReport += [PSCustomObject]@{
UserCreatedDateTime = $user.createdDateTime.ToString("dd.MM.yyyy HH:mm:ss")
DisplayName = $user.DisplayName
UserPrincipalName = $user.UserPrincipalName
MismatchedAttributes = $mismatchedAttributes -join ", "
InvalidCompanyName = $invalidCompanyName
InvalidCountry = $invalidCountry
InvalidDepartment = $invalidDepartment
}
}
}
# Convert $mismatchReport to JSON
$mismatchReportJson = $mismatchReport | ConvertTo-Json -Depth 3
Write-Output $mismatchReportJson
Click Save, then Publish the Runbook. The PowerShell Script is on my GitHub.
Setup Logic App
Azure Logic Apps let you automate workflows and integrate services using a visual designer — no code required. This is ideal for automating tasks such as sending emails, processing data, or integrating with APIs.
Create a Logic App
- Search for “Logic Apps”
In the top search bar, type Logic Apps and select the service from the list. - Click “+ Create”
- Subscription: Choose the appropriate subscription.
- Resource Group: Use an existing group or create a new one.
- Logic App Name: Choose something clear like
logic-User-MisMatch-Alerting. - Region: Select the region closest to your services.
- Plan Type: Choose Consumption for pay-per-execution, unless you have a specific integration environment.
- Click Review + Create, then Create once validated.
Logic App Overview
The Logic app structure has been kept relatively simple. Briefly explained, the Logic App runs every day at 09:00 and starts an Automation Account Job, which triggers a PowerShell Runbook. This then compares the ValidValues with the current users in the tenant. If a misconfiguration is present, an e-mail alert is sent.

Recurrence
Run every Day, could be different in your environment and timeframes.

Create job
Connect your Logic App with the Automation Account via Managed Identity.

Get job output
Collects the output from the Powershell Runbook.

Condition
Checks if the Output is not empty and a misconfiguration was found. The Alerting only triggers when there was a User found with incorrect Attributes.
length(body('Get_job_output'))

Parse JSON
The Job Output has to get parsed in a vaild JSON Format for further Transformations.

{
"type": "array",
"items": {
"type": "object",
"properties": {
"UserCreatedDateTime": {
"type": "string"
},
"DisplayName": {
"type": "string"
},
"UserPrincipalName": {
"type": "string"
},
"MismatchedAttributes": {
"type": "string"
},
"InvalidCompanyName": {
"type": "string"
},
"InvalidCountry": {
"type": "string"
},
"InvalidDepartment": {
"type": "string"
}
},
"required": [
"UserCreatedDateTime",
"DisplayName",
"UserPrincipalName",
"MismatchedAttributes",
"InvalidCompanyName",
"InvalidCountry",
"InvalidDepartment"
]
}
}
Create HTML Table
For better Visualization the parsed JSON gets transformed in a HTML Table.

Send an email (V2)
Here you can now create a Custom email Message to your Identity Governance Administrator. I used the Code View for Customization and formatting the HTML Table.

<p>Hey IT Admin,</p>
<p>
Looks like a few of your users are rocking some... let's say <strong>"creative"</strong> attributes. 👀<br>
This could seriously mess with your automations, think Dynamic Groups going rogue, Access Packages throwing tantrums, and conditional access scratching its head.
</p>
<p>
Better take a look before things get <strong>spicy</strong> in Entra ID 🔥<br>
👉 <a href="https://entra.microsoft.com">Entra ID Portal</a>
</p>
<p>Please find below the user validation issues:</p>
<div style="background-color:#f9f9f9;border:1px solid #ccc;padding:15px;border-radius:6px;font-family:sans-serif;">
@{body('Create_HTML_table')}
</div>
<p>Thanks,<br>Your friendly <strong>Cloud Overwatch Guru</strong> ☁️🕵️♂️</p>
Sample Alerting E-mails
Here you can see what the alarm looks like with data. As an example, it can be quickly identified that the user: Chris Green has an incorrect attribute in the CompanyName field: contoso.comm instead of contoso.com (with an m at the end).

After the manual cleanup and a day of waiting, you can see in the alerting that the user: Chris Green has been cleaned up.
All users that have a value: EMPTY means that this field is empty and therefore also has a misconfiguration, so the administrators must also have the field for the users updated.

Only when all users have been processed correctly will there be no more alerts. As soon as someone or a system creates a user again and an error occurs by mistake, an alert is sent immediately the next day.
Conclusion
I think the solution is good because unfortunately there is still nothing comparable in the form of a report or alert from Microsoft. It is certainly a challenge to set up the Managed Identities and authorize them properly on the resources. The data quality is very important for automation or even later for the whole topic of identity governance.
The inspiration was certainly triggered by the Access Package Builder, if you don’t know it yet, take a look here.
📎 Reference: Access Package Builder (designed by Nico Wyss)
Want to learn more, here are some interesting articles on my blog: