Table of Contents

Entra ID Directory Extensions

Have you ever wanted to save information in Entra ID, but couldn’t find an appropriate attribute to store your data?
For example, storing someones nickname in a usable fashion? Or you need a specific attribute from your HR Software for Single Sign-On or authorization? Or for Dynamic Groups?

If you have synchronized Active Directory attributes not available to Entra ID by default, you might have unintentionally created Directory Extensions – and now you need to read those values.

This information is not available in the Entra ID GUI (as of December 2023), so your only option is the Graph API – my personal choice of interface is PowerShell. And since the Microsoft documentation is missing Snippets I thought it might be interesting to write this down.


I use a separate JSON File to store information that I don’t necessarily want to share on the internet:
tenantId : GUID of the Entra ID Tenant
clientID : Application ID of my App Registration / Enterprise App
thumb : Thumbprint of the certificate I use to authenticate as the App Registration
exampleUser : User UPN of my Demo user, Object ID also works
extensionAppOID : OID – Object ID ! of the App Registration I want to associate my Directory Extension with


Connecting:

$graphConfig = ConvertFrom-Json $(Get-Content -Raw $hiddenValuesPath)
Connect-MgGraph -TenantId $graphConfig.tenantID -ClientId $graphConfig.clientID -CertificateThumbprint $graphConfig.thumb


Finding and Reading Directory Extensions

  • Use Find-MgGraphCommand "CmdLet" for the API Uri used by the CmdLet, this gives better documentation as well as the necessary permissions
  • Get-MgUser / The Graph API will only return the Properties you ask for πŸ˜‰


# Store the Directory Extension I want to look at, since the actual Name of the extension is rather unwieldy
$extension  = Get-MgDirectoryObjectAvailableExtensionProperty  | where Name -match "exampleExtension"

# "I try to use as few Microsoft.Graph modules as possible because updating takes forever"
# $extension = (Invoke-MgGraphRequest POST "/v1.0/directoryObjects/getAvailableExtensionProperties" -OutputType PSObject).Value | Where-Object Name -match "exampleExtension"

# Get the Value for a specific User
$user = Get-MgUser -UserId $graphConfig.exampleUser -Property Displayname, Id, UserPrincipalName, $extension.Name 

# Define a Calculated Property for Select-Object, that resolves the nesting of AdditionalProperties
$extensionValueExpr = @{Name = "$($extension.Name)"; Expression = {$_.AdditionalProperties.$($extension.Name)}}
$user | Select-Object Displayname, Id, UserPrincipalName, $extensionValueExpr | Format-Table

Result:

Exemple Result 1


Alternatively, we can filter all Users by the Extension Attributes:

$extension  = Get-MgDirectoryObjectAvailableExtensionProperty  | where Name -match "nickName"
$users = Get-MgUser -Filter "startswith($($extension.Name),'I')" -Property Displayname, Id, UserPrincipalName, $extension.Name 

# Define a Calculated Property for Select-Object, that resolves the nesting of AdditionalProperties
$extensionValueExpr = @{Name = "nickName"; Expression = {$_.AdditionalProperties.$($extension.Name)}}
$users | Select-Object Displayname, Id, UserPrincipalName, $extensionValueExpr | Format-Table

Result:

Example Result 2


Create an Extension

! This should not be done with the extensions in use by Entra ID Connect Sync – the Source of Truth for these attributes should remain OnPremises
Use the Entra ID Connect Sync Wizard to add Extensions


# There must always be an assigned app, but otherwise the Extensions can be assigned rather broadly
$oid = $graphConfig.extensionAppOID
$params = @{
    name = "freshExtension"
    dataType = "String"
    targetObjects = @("User")
}
New-MgApplicationExtensionProperty -ApplicationId $oid @params

# If I were to use the App ID of my Registration with the attached Permissions I would have to fetch the ID from the Graph API:
# $oid = (Get-MgApplication -Filter $("AppId eq '{0}'" -f $graphConfig.clientID)).Id

# Less Modules, Params from Above are used:
# Invoke-MgGraphRequest POST "/v1.0/applications/$oid/extensionProperties" -Body $params

Result:

Example Result 3


Set the Extension on a User

! This should not be done with the extensions in use by Entra ID Connect Sync – the Source of Truth for these attributes should remain OnPremises
Use OnPrem AD Management solutions to change Values (Set-ADUser, IAM, etc.)

$extension  = Get-MgDirectoryObjectAvailableExtensionProperty  | where Name -match "freshExtension"

# We could also set multiple Extensions with one Request, but then we would have to explicitly Name them all :)
$params = @{
    "$($extension.Name)" = "Hello, this is a fresh value"
}
Update-MgUser -UserId $graphConfig.exampleUser -BodyParameter $params

# The Update does not return a result on Success, so lets have a look:
$user = Get-MgUser -UserId $graphConfig.exampleUser -Property Displayname, Id, UserPrincipalName, $extension.Name 

# Define a Calculated Property for Select-Object, that resolves the nesting of AdditionalProperties
$extensionValueExpr = @{Name = "freshExtension"; Expression = {$_.AdditionalProperties.$($extension.Name)}}
$user | Select-Object Displayname, Id, UserPrincipalName, $extensionValueExpr | Format-Table

Result:
Example Result 4


Read all Extension Attributes

If we want to see all extension properties at once, things get a little more complicated – do let me know if there is a better way πŸ˜‰

# Get all Extension Properties - if run on a regular basis these could be cached
$extensions = Get-MgDirectoryObjectAvailableExtensionProperty

# Standard User Properties we want to Fetch
$properties = @("Displayname", "Id", "UserPrincipalName") 

# Fetch all users and the Extension Properties
$users = Get-MgUser -All -Property ($properties + $extensions.Name)

$allUsersParsed = [System.Collections.Arraylist]::new()

# Flatten user data by merging extension properties from nested hashtables into a single-level hashtable
# Also filter out unnecessary fields from the full Graph User Schema to leave populated properties
Foreach ($u in $users){
    $userParsed = @{}
    Foreach ($prop in $properties) {
        $userParsed.$prop = $u.$prop
    }
    $userParsed += $u.AdditionalProperties
    $allUsersParsed.Add([pscustomobject]$userParsed) | Out-Null
}

$allUsersParsed | Format-Table ($properties + $extensions.Name)

Result:

Example Result 5


Conclusion

So we can now read and create directory extensions – very cool. But where do we go from here? I did this for fun, but let me know what useful things you used this knowledge for😁


I won’t be moderating comments and I don’t want your email address; please join the discussion on my corresponding LinkedIn Post.


If you are interested in the things I do, please follow me on LinkedIn.


Further Reading



Updates:

  1. 08.12.23 – Improvements of Code Clarity around Select-Object
Last modified: 5. January 2024