The set of Exchange custom attributes, known as CustomAttribute1-15 or extensionAttribute1-15, have long been used to address scenarios where you need a quick and easy way to stamp additional metadata on directory objects. Thus, it comes as no surprise that customers want to work with said attributes in Microsoft 365/Entra ID as well, and after some (looooong) persuading, Microsoft added support for them in the Graph API, via the onPremisesExtensionAttributes resource.
Being true to itself, Microsoft only addressed part of the story however, and the overall implementation is at best a “minimal viable product”, or in other words it leaves a lot to be desired. For example group objects within the Graph are yet to support extension attributes, whereas we got support for them in device objects. The implementation also does not cover the set of 5 multi-valued ExtensionCustomAttribute1-5, and there are some very limiting dependencies on source of authority noted in the article above. TL;DR you should be using Exchange Online PowerShell cmdlets to work with said attributes.
But if you are reading an article with Graph SDK in its title, I imagine you are looking for a solution based on the Graph API and/or the corresponding PowerShell cmdlets. So, let’s go over some examples on how you can work with custom attributes via the Graph SDK for PowerShell. While there’s nothing new in this space, there is an alarming lack of coverage for the basic management operations in the documentation and on the internet as general, so hopefully this article will be of some use.
Do note that all the examples below assume you have the necessary permissions. If running in a delegate context, this means at least User.Read.All and a matching read-only Entra role for fetching the attribute values, and User.ReadWrite.All as well as matching admin role for modifying them. The User management Entra ID role should be the least privileged one. I have also included some examples for the Entra module, though in most cases, the differences are minute.
Fetching and reporting on custom attributes
The first thing to note is that extensionAttribute1-15 are not returned by default when using the GA (v1.0) of the Graph. I have seen many people struggle on this point, even though this concept is not unique to the Graph API. To get the attributes, you need to specifically request the OnPremisesExtensionAttributes blob, which is done by passing it to the –Property parameter. In all fairness, there are SDK-specific bits that contribute to the issue, and Microsoft can definitely do a better job here. The screenshot below illustrates that:
Get-MgUser -UserId xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -Property OnPremisesExtensionAttributes Get-MgUser -UserId xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -Property OnPremisesExtensionAttributes | select OnPremisesExtensionAttributes
The Entra module does provide a somewhat better experience, and has further improvements planned, which will hopefully help alleviate such confusions going forward. Until that happens, you will need to continue using something like this:
#Fetch custom attributes for a given user via the Graph SDK for PowerShell Get-MgUser -UserId xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -Property OnPremisesExtensionAttributes | select -ExpandProperty OnPremisesExtensionAttributes | select * #Fetch custom attributes for a given user via the Entra module Get-EntraUser -UserId xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -Property OnPremisesExtensionAttributes | select -ExpandProperty OnPremisesExtensionAttributes
You can of course also perform the same operation against multiple (or all) users. Here’s a quick snippet that allows you to export the set of users along with any associated custom attributes to a CSV file:
#Export all custom attributes to a CSV file via the Graph SDK for PowerShell Get-MgUser -All -Property Id,UserPrincipalName,OnPremisesExtensionAttributes | select Id,UserPrincipalName -ExpandProperty OnPremisesExtensionAttributes | select -ExcludeProperty AdditionalProperties | Export-Csv -nti D:\Downloads\aaaa.csv #Export all custom attributes to a CSV file via the Entra module Get-EntraUser -All -Select Id,UserPrincipalName,OnPremisesExtensionAttributes | select Id,UserPrincipalName -ExpandProperty OnPremisesExtensionAttributes | Export-Csv -nti D:\Downloads\aaaa.csv
You can also filter by custom attribute values, including null values. Keep in mind however that those are an advanced query requests, thus requires additional steps and are not supported by the Entra module currently. Here are some examples:
#Filter all users with extensionAttribute7 value of "IsManager" Get-MgUser -Filter "onPremisesExtensionAttributes/extensionAttribute7 eq 'IsManager'" -ConsistencyLevel eventual -CountVariable count #Filter on not-matching values Get-MgUser -Filter "onPremisesExtensionAttributes/extensionAttribute7 ne 'IsManager'" -ConsistencyLevel eventual -CountVariable count -Top 2 #Filter on null values Get-MgUser -Filter "onPremisesExtensionAttributes/extensionAttribute7 eq null" -ConsistencyLevel eventual -CountVariable count -Top 2 #Combine multiple filters Get-MgUser -Filter "onPremisesExtensionAttributes/extensionAttribute15 eq 'Teams' and accountEnabled eq true" -ConsistencyLevel eventual -CountVariable count
Managing custom attributes
To set the value of a given custom attribute, you have to encapsulate it as part of the onPremisesExtensionAttributes blob. A few variations of the same API request can be used via the Graph SDK for PowerShell:
#Set a value for extensionAttribute4 Update-MgUser -UserId db19b7fc-dfd7-4aa5-9c13-3cc67bf42ce4 -OnPremisesExtensionAttributes @{"extensionAttribute4"="Value"} #Same, but more convoluted Update-MgUser -UserId db19b7fc-dfd7-4aa5-9c13-3cc67bf42ce4 -BodyParameter @{"OnPremisesExtensionAttributes" = @{"extensionAttribute4"="Value"}} #And another variation $params = @{ onPremisesExtensionAttributes = @{ extensionAttribute4 = "value" } } Update-MgUser -UserId db19b7fc-dfd7-4aa5-9c13-3cc67bf42ce4 -BodyParameter $params
Only the attribute(s) specified as part of the payload will have their values modified, you don’t have to worry about invertedly overwriting the values for the remaining attributes. To update an existing value, you can use the exact same methods listed above. Removing an existing value is where things get interesting, as the Graph SDK is notorious for its bad handling of null values, a problem that has persisted for years. While the SDK will happily allow you to pass a null value for any given custom attribute, the actual API call made uses an empty string instead, as demonstrated on the screenshot below:
The “good” news is that Microsoft decided to equate null values to empty string on the backend, so yay, we have a solution. If you can call it that, but for the purposes of this article, it should do. Without further ado, here is how to clear the value of any custom attribute:
#Clear the value of extensionAttribute4 Update-MgUser -UserId 51485be9-c8bb-4e78-a6db-fccd4f392bdf -OnPremisesExtensionAttributes @{"extensionAttribute4"=$null} #Clear the value of extensionAttribute5 via -BodyParameter Update-MgUser -UserId 51485be9-c8bb-4e78-a6db-fccd4f392bdf -BodyParameter @{"OnPremisesExtensionAttributes" = @{"extensionAttribute5"=$null}}
As the Entra module doesn’t expose an –OnPremisesExtensionAttributes parameter, you will need to use –BodyParameter instead. Other than that, the syntax remains the same, so you can leverage the examples above by simply replacing Update-MgUser with Update-EntraUser.
The aforementioned issue with null values is hardly the only problem with the Graph SDK for PowerShell either. The lack of proper pipeline support greatly hampers the usability of the module, and forces additional and unnecessary complications to many basic scenarios. Still, PowerShell being PowerShell, we can use it to automate some bulk operations. For example, let’s consider a scenario where we need to update the extensionAttribute5 value for multiple users. Below you can find examples on how to fetch a list of users based on various criteria, or from a CSV file, and update the extensionAttribute5 value.
#Update extensionAttribute5 for all users in the Sales department Get-MgUser -Filter "department eq 'Sales'" | % { Update-MgUser -UserId $_.Id -OnPremisesExtensionAttributes @{"extensionAttribute5"="Sales"} } #Update extensionAttribute5 for all users in the "Marketing DL" group $group = Get-MgGroup -Filter "displayName eq 'manager'" Get-MgGroupMemberAsUser -GroupId $group.Id | % { Update-MgUser -UserId $_.Id -OnPremisesExtensionAttributes @{"extensionAttribute5"="Sales"} } #Read a set of users from a CSV file and update their extensionAttribute5 Import-Csv .\testtttt.csv | % { Update-MgUser -UserId $_.Id -OnPremisesExtensionAttributes @{"extensionAttribute5"="Sales"} }
The Entra module on the other hand does have pipeline support, so things are a bit easier therein. Here are some examples:
#Basic pipeline support demonstration Get-EntraUser -SearchString Melissa | Set-EntraUser -BodyParameter @{"onPremisesExtensionAttributes" = @{"extensionAttribute5"="Value"}} #Bulk update extensionAttribute5 for all users in Seattle Get-EntraUser -Filter "city eq 'Seattle'" | Set-EntraUser -BodyParameter @{"onPremisesExtensionAttributes" = @{"extensionAttribute5"=$null}}
Some additional remarks
One last thing that needs to be mentioned is that updating custom attributes is only possible in specific scenarios. The issue here stems from the source of authority concept, with an added Graph-specific “touch”. The short version is, we can only manage custom attributes via the Graph API/SDK for users created in the cloud. If the tenant has at any point enabled directory synchronization, the set of extensionAttributes1-15 are read-only and cannot be managed via the Graph. This holds true even if you disable directory synchronization and will affect any existing objects… but not newly created ones. Because consistency!
Long story short, if you try to update the value of any of the extensionAttributes1-15 for a (previously) synced user, you end up with the following error message:
Update-MgUser -UserId db19b7fc-dfd7-4aa5-9c13-3cc67bf42ce4 -BodyParameter @{"OnPremisesExtensionAttributes" = @{"extensionAttribute4"="aaaaa"}}
This doesn’t necessarily mean that said attribute(s) cannot be updated, they simply cannot be updated via the Graph API and related tools. For any users that aren’t currently syncing from on-premises AD however, you can still update said attributes via the Exchange Online PowerShell cmdlets.
#Exchange Online PowerShell cmdlets support more scenarios Set-MailUser db19b7fc-dfd7-4aa5-9c13-3cc67bf42ce4 -CustomAttribute4 "aaaaa" #Verify the updated value via the Graph SDK Get-MgUser -UserId db19b7fc-dfd7-4aa5-9c13-3cc67bf42ce4 -Property OnPremisesExtensionAttributes | select -ExpandProperty OnPremisesExtensionAttributes | select ExtensionAttribute4 ExtensionAttribute4 ------------------- aaaaa
While I’ve been using “custom attributes” and “extension attributes” interchangeably throughout this article, some confusion might arise because of various other similarly-named properties and extension mechanisms. So let me clarify one more time that we’re only and specifically talking about the set of 15 extensionAttribute1-15 properties, aka Exchange custom attributes. For an overview of all other “custom” attributes and various directory extensions we have in the Graph/Entra, you can refer to my previous articles.
Related to the above, it’s worth reiterating that the 5 multi-valued ExtensionCustomAttribute1-5 are not covered by the Graph API and can only be managed via Exchange Online PowerShell. Similarly, there is no support for querying or setting values for custom attributes on group objects in the Graph API. Same goes for contacts, mail users, and any other Exchange recipient types that “natively” support them. On the other hand, the Graph does allow you to configure extensionAttribute1-15 for device objects. There are some caveats though, such as the different resource, different output format (of course!) and the highly privileged permissions needed to work with them in the delegate context… a good topic for another article.
1 thought on “Working with custom attributes via the Graph SDK for PowerShell and the Entra module”