In on-premises Active Directory, we rely on Custom Extension attributes 1 to 15. Once those are used up, extending further means a schema extension. Several of these slots are already mapped into Entra ID and used to park attributes or tags, mainly to drive Entra identity lifecycle events and dynamic group membership rules.
For new requirements like this, Microsoft offers two ways to store custom values in Entra ID:
- Entra ID Custom Security Attributes
- Entra Directory Extension Attributes
Option 1: Custom Security Attributes
These live under Entra ID Governance, in Protection > Custom security attributes.
Limitation: they are explicitly not supported in dynamic group membership rules. Microsoft's own documentation confirms this: these attributes are not exposed to the dynamic group evaluation engine, so there is no way to reference them in a rule, regardless of syntax.
This rules them out for our use case.
Option 2: Directory Extension Attributes
Also called custom extension properties, these are registered against an app registration in the tenant.
Key characteristics:
- Fully supported in dynamic group rules, using the syntax
user.extension_<appId>_<AttributeName> -eq "value" - Cloud-native and writable via Graph regardless of on-prem sync status. Entra Connect has no ownership of them, so there is no sync-conflict risk (unlike extensionAttribute1 to 15)
- No Hybrid Runbook Worker required. A standard cloud Automation Account with a managed identity is sufficient
This is the right fit for our scenario.
How Directory Extension Attributes Work
Ownership through an app registration
Every extension attribute must be registered under an app registration, and that app becomes its owner. This is why a dedicated app is created for the purpose. It is not optional scaffolding; it is how the feature is designed to work. One app can own multiple extension attributes, and different apps can each own their own sets.
Naming convention
Once registered, the attribute receives a permanent, auto-generated name:
extension_<appId-without-dashes>_<attributeName>
Example: an attribute named isPrivilegedUser, registered under app c1c518d8-ee7d-48af-8d89-eb4da4c8f20e, becomes:
user.extension_c1c518d8ee7d48af8d89eb4da4c8f20e_isPrivilegedUser
This is the only name that exists for the attribute. It is used consistently across Graph API calls, dynamic group rules, and PowerShell. There is no separate friendly name exposed anywhere.
Two flavors of directory extension attributes
Cloud-native attributes (what we are building) Created directly via Graph or PowerShell against an app registration we control. Fully cloud-writable, with no dependency on sync.
Entra Connect synced attributes If the on-prem AD schema is extended and those custom attributes are synced up, Entra Connect auto-registers them as directory extensions as well, but under its own app called the Tenant Schema Extension App, not one we create. In this case, on-prem AD remains the source of truth, and the cloud side is read-only for those values. This is the same ownership constraint we already have with extensionAttribute1 to 15, just under a different app.
The Tenant Schema Extension App
When Microsoft Entra Connect is installed with the classic sync engine (not Cloud Sync) and custom on-prem AD attributes are selected for sync, the installer automatically registers a special application called the Tenant Schema Extension App. It is created once, during installation. We never create it ourselves, and it does not appear in the standard app list unless the filter is switched to "All Applications."
During the Entra Connect configuration wizard's custom settings step, it presents a list of AD schema attributes eligible for extension. Whichever ones are selected get registered as directory extension attributes under this single app, using the same naming pattern described above:
extension_<TenantSchemaExtensionAppId>_<attributeName>
The key point: every synced attribute shares the same app ID, the Tenant Schema Extension App's own ID. This is different from our setup, where a separate app is created specifically for one custom attribute. On-prem synced attributes all funnel through this one shared app.
Classic sync vs Cloud Sync
| Classic Entra Connect | Entra Connect Cloud Sync | |
|---|---|---|
| Owning app | Tenant Schema Extension App | App with identifier URI api://<tenantId>/CloudSyncCustomExtensionsApp |
| App creation | Auto-created at install time | Not guaranteed to exist; created on demand |
For Cloud Sync, Microsoft's documentation shows a check-and-create pattern:
$cloudSyncCustomExtApp = Get-MgApplication -Filter "identifierUris/any(uri:uri eq 'api://$tenantId/CloudSyncCustomExtensionsApp')"
If this returns empty, the app must be created. If the app exists but has no service principal, that also needs to be created:
POST https://graph.microsoft.com/v1.0/servicePrincipals
{ "appId": "<application appId>" }
Our Custom App vs the Tenant Schema Extension App
| Our custom app | Tenant Schema Extension App | |
|---|---|---|
| Created by | Us, manually, via Graph/PowerShell | Entra Connect, automatically, at install time |
| Source of truth | Cloud (writable via Graph anytime) | On-prem AD (Entra Connect owns it, syncs one-way) |
| Used for | The isPrivilegedUser tagging we built | Any AD attribute exposed via Entra Connect's wizard |
Setup: Creating a Cloud-Native Attribute
There is no portal UI for this step. It is Graph or PowerShell only.
Connect-MgGraph -Scopes "Application.ReadWrite.All"
$app = New-MgApplication -DisplayName "Directory Extensions Owner App"
New-MgApplicationExtensionProperty -ApplicationId $app.Id -BodyParameter @{
Name = "isPrivilegedUser"
DataType = "String"
TargetObjects = @("User")
}
New-MgServicePrincipal -AppId $app.AppId
How to add another Extension for same app.
To add another attribute, reuse the same app — don't run New-MgApplication again, or you'll end up with a second, duplicate "Directory Extensions Owner App" and a confusing mess of two apps owning different attributes.
Step 1 — Get the existing app's ID (don't recreate it):
powershell
$app = Get-MgApplication -Filter "displayName eq 'Directory Extensions Owner App'"
$app.IdThis should return 7ffcea56-09ff-4d28-982d-94de68690e36 — the same app you already have a service principal for.
Step 2 — Register the new extension property on that same app:
powershell
New-MgApplicationExtensionProperty -ApplicationId $app.Id -BodyParameter @{
Name = "yourNewAttributeName"
DataType = "String"
TargetObjects = @("User")
}Just swap yourNewAttributeName for whatever you want to call it.
That's it — no new service principal needed. Since this attribute is registered under the same app that already has a working service principal, it inherits that and should be immediately writable via Update-MgUser -AdditionalProperties, without repeating the New-MgServicePrincipal step.
Step 3 — Confirm it's there:
powershell
Get-MgApplicationExtensionProperty -ApplicationId $app.IdYou should now see both isPrivilegedUser and your new attribute listed side by side, both usable in dynamic group rules and via Graph once you copy the exact name field for each.
Q&A
- Why create an app registration for this at all?
Entra ID's user profile works like a fixed form with predefined fields: name, email, job title, and so on. A new field cannot simply be invented; Entra needs something to own that field before it will let one be created. An app registration is a container that can claim ownership of custom attributes. It is not being used as an app in any functional sense: no sign-in, no permissions consumed by it. It exists purely so the attribute has a home, which is why it is named descriptively, such as "Directory Extensions Owner App."
- How does the attribute get added to a user?
Once registered on the app, the attribute becomes a real, optional field on every user object in the tenant, effectively a new extension attribute slot that did not exist before. It stays empty until something writes a value into it. For each user identified as an admin, the script opens that user's record and writes a value into the field. It is a normal write operation, just to a field we created rather than a built-in one.
- Can this attribute be seen in the Entra ID portal?
Not on a user's profile page; there is no tab listing custom extension attributes and their values for a user. Values can only be viewed via Graph or PowerShell, for example:
Get-MgUser -Property "extension_..."
There is one place it does surface in the UI: when building a dynamic group rule in the Entra admin center, the rule editor has a "Get custom extension properties" button. Pointing it at the owning app and refreshing makes the attribute selectable in the rule builder dropdown. Outside of that workflow, it is not visible anywhere in the portal.
- Does this conflict with the on-prem extensionAttribute1 to 15?
No. Despite the similar naming, these are separate, unrelated storage locations.
extensionAttribute1 to 15 are legacy AD schema attributes. For synced users, Entra Connect owns them and pushes values one-way from on-prem AD to the cloud, which is exactly why they could not be used here: writing to them from the cloud side risks being overwritten by the next sync cycle.
Our new attribute, extension_<appid>_isPrivilegedUser, lives in a completely separate namespace tied to our app's ID. It is cloud-native, cloud-writable, and Entra Connect has no awareness of it or authority over it.

0 comments:
Post a Comment